feat(teacher): 连接 Yuxi 老师并提供项目与会话只读工具
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { hostApiFetch, createHostEventSource, ensureHostApiToken } from './host-api';
|
||||
import type {
|
||||
TeacherAvailability,
|
||||
TeacherCatalog,
|
||||
TeacherDefinition,
|
||||
TeacherSend,
|
||||
TeacherTopic,
|
||||
@@ -12,6 +13,7 @@ export function teacherTopicsPath(projectId: string, sourceId: string) {
|
||||
: `/api/coding/projects/${encodeURIComponent(projectId)}/conversations/${encodeURIComponent(sourceId)}/teacher-topics`;
|
||||
}
|
||||
export const teacherApi = {
|
||||
catalog: () => hostApiFetch<TeacherCatalog>('/api/coding/teacher/teachers'),
|
||||
config: () =>
|
||||
hostApiFetch<TeacherAvailability & { definition: TeacherDefinition | null }>(
|
||||
'/api/coding/teacher/config'
|
||||
@@ -21,10 +23,10 @@ export const teacherApi = {
|
||||
'/api/coding/teacher-preview?draftRevision=' + draftRevision
|
||||
),
|
||||
list: (base: string) => hostApiFetch<TeacherTopicList>(base),
|
||||
create: (base: string, draftRevision?: number, sampleContext?: string) =>
|
||||
create: (base: string, draftRevision?: number, sampleContext?: string, teacherVersion?: number) =>
|
||||
hostApiFetch<TeacherTopic>(base, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ draftRevision, sampleContext }),
|
||||
body: JSON.stringify({ draftRevision, sampleContext, teacherVersion }),
|
||||
}),
|
||||
read: (base: string, id: string) =>
|
||||
hostApiFetch<TeacherTopic>(base + '/' + encodeURIComponent(id)),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MessageCircle, Plus, X } from 'lucide-react';
|
||||
import { teacherApi, teacherTopicsPath } from '@/lib/coding-teacher';
|
||||
import type {
|
||||
TeacherDefinition,
|
||||
TeacherCatalog,
|
||||
TeacherReference,
|
||||
TeacherSend,
|
||||
TeacherTopic,
|
||||
@@ -36,6 +37,9 @@ export function TeacherChatPanel({
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [legacyEnabled, setLegacyEnabled] = useState(true);
|
||||
const [teachers, setTeachers] = useState<TeacherCatalog['items']>([]);
|
||||
const [selectedTeacherVersion, setSelectedTeacherVersion] = useState<number>();
|
||||
const pending = useRef<TeacherSend | null>(null);
|
||||
const generation = useRef(0);
|
||||
const topicRef = useRef(topic);
|
||||
@@ -66,6 +70,17 @@ export function TeacherChatPanel({
|
||||
if (alive) {
|
||||
setDefinition(config.definition);
|
||||
setEnabled(config.enabled);
|
||||
setLegacyEnabled(config.enabled);
|
||||
}
|
||||
if (!draftRevision) {
|
||||
const catalog = await teacherApi.catalog();
|
||||
if (alive) {
|
||||
setTeachers(catalog.items);
|
||||
const selected = catalog.items.find((item) => item.is_default) ?? catalog.items[0];
|
||||
setSelectedTeacherVersion(selected?.version);
|
||||
setEnabled(catalog.items.length > 0);
|
||||
if (selected) setDefinition(selected.definition);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (alive) setError(e instanceof Error ? e.message : '老师配置暂不可用');
|
||||
@@ -155,7 +170,12 @@ export function TeacherChatPanel({
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const next = await teacherApi.create(base, draftRevision, sampleContext);
|
||||
const next = await teacherApi.create(
|
||||
base,
|
||||
draftRevision,
|
||||
sampleContext,
|
||||
selectedTeacherVersion
|
||||
);
|
||||
if (version === generation.current) {
|
||||
consume(next);
|
||||
setDefinition(next.definition);
|
||||
@@ -177,9 +197,15 @@ export function TeacherChatPanel({
|
||||
let current = topicRef.current;
|
||||
try {
|
||||
if (!current) {
|
||||
current = await teacherApi.create(base, draftRevision, sampleContext);
|
||||
current = await teacherApi.create(
|
||||
base,
|
||||
draftRevision,
|
||||
sampleContext,
|
||||
selectedTeacherVersion
|
||||
);
|
||||
if (version !== generation.current) return;
|
||||
consume(current);
|
||||
setDefinition(current.definition);
|
||||
}
|
||||
const previous = pending.current;
|
||||
const input =
|
||||
@@ -206,6 +232,11 @@ export function TeacherChatPanel({
|
||||
const running = topic?.requests.find(
|
||||
(request) => request.status === 'preparing' || request.status === 'running'
|
||||
);
|
||||
const topicEnabled = !topic
|
||||
? enabled
|
||||
: topic.definition.config_id
|
||||
? teachers.some((item) => item.teacher_id === topic.definition.config_id)
|
||||
: legacyEnabled;
|
||||
return (
|
||||
<aside
|
||||
className="flex h-full min-h-0 w-full flex-col border-l border-border bg-background"
|
||||
@@ -243,6 +274,25 @@ export function TeacherChatPanel({
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
{!draftRevision && teachers.length > 0 && (
|
||||
<label className="mx-3 mt-2 text-xs text-muted-foreground">
|
||||
新话题使用的老师
|
||||
<select
|
||||
aria-label="新话题使用的老师"
|
||||
value={selectedTeacherVersion ?? ''}
|
||||
disabled={busy}
|
||||
onChange={(event) => setSelectedTeacherVersion(Number(event.target.value))}
|
||||
className="mt-1 w-full rounded border bg-background p-2 text-foreground"
|
||||
>
|
||||
{teachers.map((item) => (
|
||||
<option key={item.teacher_id} value={item.version}>
|
||||
{item.definition.name}
|
||||
{item.is_default ? '(默认)' : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{topics.items.length > 0 && (
|
||||
<select
|
||||
aria-label="老师话题"
|
||||
@@ -293,6 +343,11 @@ export function TeacherChatPanel({
|
||||
{request.response ||
|
||||
(['running', 'preparing'].includes(request.status) ? '老师正在思考…' : '')}
|
||||
</div>
|
||||
{request.progress && ['running', 'preparing'].includes(request.status) && (
|
||||
<p role="status" className="text-xs text-muted-foreground">
|
||||
{request.progress}
|
||||
</p>
|
||||
)}
|
||||
{request.omittedMessages > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
本轮上下文已省略 {request.omittedMessages} 条较早消息。
|
||||
@@ -300,7 +355,9 @@ export function TeacherChatPanel({
|
||||
)}
|
||||
{(request.truncatedMessages ?? 0) > 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{draftRevision ? '较长的上下文已节选。' : '较长的上下文已节选,老师可按需读取原文。'}
|
||||
{draftRevision
|
||||
? '较长的上下文已节选。'
|
||||
: '较长的上下文已节选,老师可按需读取原文。'}
|
||||
</p>
|
||||
)}
|
||||
{request.error && <p className="text-xs text-destructive">{request.error}</p>}
|
||||
@@ -338,7 +395,7 @@ export function TeacherChatPanel({
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!enabled && (
|
||||
{!topicEnabled && (
|
||||
<p className="px-4 text-xs text-muted-foreground">老师暂未开放,历史仍可查看。</p>
|
||||
)}
|
||||
<form
|
||||
@@ -385,7 +442,7 @@ export function TeacherChatPanel({
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busy || !enabled || !text.trim()}
|
||||
disabled={busy || !topicEnabled || !text.trim()}
|
||||
className="rounded-lg bg-brand px-4 py-2 text-xs text-white disabled:opacity-40"
|
||||
>
|
||||
{busy ? '正在发送…' : '提问'}
|
||||
|
||||
Reference in New Issue
Block a user