feat: 在顶栏并排展示下发智能体并直接切换
This commit is contained in:
1 parent
39b03a1628
commit
a10cf00015
15 files changed
+291
-59
No files matched your search
@@ -28,6 +28,7 @@ export function TeacherChatPanel({
|
||||
externalTopic,
|
||||
onTopicChange,
|
||||
onComposingChange,
|
||||
selectedAgent,
|
||||
}: {
|
||||
projectId: string;
|
||||
sourceId: string;
|
||||
@@ -42,17 +43,21 @@ export function TeacherChatPanel({
|
||||
externalTopic?: TeacherTopic | null;
|
||||
onTopicChange?(topic: TeacherTopic): void;
|
||||
onComposingChange?(composing: boolean): void;
|
||||
selectedAgent?: TeacherCatalog['items'][number];
|
||||
}) {
|
||||
const base = teacherTopicsPath(projectId);
|
||||
const account = useAuthStore((state) => state.user?.userId ?? state.user?.username ?? 'signed-out');
|
||||
// Retain the existing project draft key; its suffix is a storage identifier, not a role.
|
||||
const draftKey = !draftRevision ? `makelore-consultation-draft:${account}:${projectId}:teacher` : null;
|
||||
const projectDraftKey = `makelore-consultation-draft:${account}:${projectId}:teacher`;
|
||||
const draftKey = !draftRevision ? selectedAgent ? `makelore-consultation-draft:${account}:${projectId}:agent:${selectedAgent.teacher_id}` : projectDraftKey : null;
|
||||
const [saved] = useState<{ text?: string; references?: TeacherReference[]; pending?: TeacherSend }>(() => {
|
||||
try { return draftKey ? JSON.parse(localStorage.getItem(draftKey) ?? '{}') : {}; } catch { return {}; }
|
||||
});
|
||||
const [legacyDraft] = useState<{ text?: string; references?: TeacherReference[] }>(() => {
|
||||
try { return draftKey ? JSON.parse(localStorage.getItem(`makelore-consultation-draft:${account}:${projectId}:friend`) ?? '{}') : {}; } catch { return {}; }
|
||||
});
|
||||
const [unassignedDraft] = useState<{ text?: string; references?: TeacherReference[] }>(() => {
|
||||
try { return selectedAgent ? JSON.parse(localStorage.getItem(projectDraftKey) ?? '{}') : {}; } catch { return {}; }
|
||||
});
|
||||
const persistDraft = useCallback((text: string, references: TeacherReference[], pending?: TeacherSend | null) => {
|
||||
if (!draftKey) return;
|
||||
try { localStorage.setItem(draftKey, JSON.stringify({ text, references, pending })); } catch { /* Draft remains in the open panel. */ }
|
||||
@@ -65,6 +70,9 @@ export function TeacherChatPanel({
|
||||
const avatar = getAgentAvatarSrc(definition?.avatar_id);
|
||||
const externalTopicRef = useRef(externalTopic);
|
||||
externalTopicRef.current = externalTopic;
|
||||
const selectedAgentId = selectedAgent?.teacher_id;
|
||||
const selectedAgentRef = useRef(selectedAgent);
|
||||
selectedAgentRef.current = selectedAgent;
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const focusedOnce = useRef(false);
|
||||
const [topics, setTopics] = useState<TeacherTopicList>({ items: [], lastSelectedTopicId: null });
|
||||
@@ -118,11 +126,12 @@ export function TeacherChatPanel({
|
||||
[persistDraft]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (externalTopic && externalTopic.projectId === projectId) {
|
||||
if (externalTopic && externalTopic.projectId === projectId
|
||||
&& (!selectedAgentId || externalTopic.definition.config_id === selectedAgentId)) {
|
||||
consume(externalTopic);
|
||||
setDefinition(externalTopic.definition);
|
||||
}
|
||||
}, [externalTopic, projectId, consume]);
|
||||
}, [externalTopic, projectId, consume, selectedAgentId]);
|
||||
useEffect(() => { if (topic) onTopicChange?.(topic); }, [topic, onTopicChange]);
|
||||
useEffect(() => {
|
||||
if (!busy && !focusedOnce.current) {
|
||||
@@ -150,10 +159,13 @@ export function TeacherChatPanel({
|
||||
const catalog = await teacherApi.catalog();
|
||||
if (alive) {
|
||||
setTeachers(catalog.items);
|
||||
const selected = catalog.items.find((item) => item.is_default) ?? catalog.items[0];
|
||||
const selected = selectedAgentId
|
||||
? catalog.items.find((item) => item.teacher_id === selectedAgentId)
|
||||
: catalog.items.find((item) => item.is_default) ?? catalog.items[0];
|
||||
setSelectedTeacherVersion(selected?.version);
|
||||
setEnabled(catalog.items.length > 0 || (config.enabled && !config.definition?.config_id));
|
||||
if (selected) setDefinition(selected.definition);
|
||||
setEnabled(selectedAgentId ? Boolean(selected) : catalog.items.length > 0 || (config.enabled && !config.definition?.config_id));
|
||||
const visibleAgent = selected ?? selectedAgentRef.current;
|
||||
if (visibleAgent) setDefinition(visibleAgent.definition);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -163,11 +175,14 @@ export function TeacherChatPanel({
|
||||
const listed = await teacherApi.list(base);
|
||||
if (!alive) return;
|
||||
setTopics(listed);
|
||||
if (externalTopicRef.current) {
|
||||
const previous = listed.items.find((item) => item.id === listed.lastSelectedTopicId
|
||||
&& (!selectedAgentId || item.teacherId === selectedAgentId))
|
||||
?? (selectedAgentId ? listed.items.find((item) => item.teacherId === selectedAgentId) : undefined);
|
||||
if (externalTopicRef.current && (!selectedAgentId || externalTopicRef.current.definition.config_id === selectedAgentId)) {
|
||||
consume(externalTopicRef.current);
|
||||
setDefinition(externalTopicRef.current.definition);
|
||||
} else if (listed.lastSelectedTopicId) {
|
||||
const current = await teacherApi.read(base, listed.lastSelectedTopicId);
|
||||
} else if (previous) {
|
||||
const current = await teacherApi.read(base, previous.id);
|
||||
if (alive && (!draftRevision || current.draftRevision === draftRevision)) {
|
||||
consume(current);
|
||||
setDefinition(current.definition);
|
||||
@@ -185,7 +200,7 @@ export function TeacherChatPanel({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
generation.current++;
|
||||
};
|
||||
}, [base, draftRevision, label, consume]);
|
||||
}, [base, draftRevision, label, consume, selectedAgentId]);
|
||||
useEffect(() => {
|
||||
if (!topic?.id || legacyFriend) return;
|
||||
const streamTopicId = topic.id;
|
||||
@@ -375,18 +390,6 @@ export function TeacherChatPanel({
|
||||
{onExpand && <button className="consult-icon" aria-label={expanded ? '恢复咨询栏宽度' : '展开咨询栏'} onClick={onExpand}><Maximize2 className="h-4 w-4" /></button>}
|
||||
{onClose && <button className="consult-icon" aria-label={`关闭${label}`} onClick={onClose}><X className="h-4 w-4" /></button>}
|
||||
</header>
|
||||
{!draftRevision && teachers.length > 0 && (
|
||||
<label className="px-5 pb-2 text-[11px] text-muted-foreground">
|
||||
新话题使用的智能体
|
||||
<select aria-label="新话题使用的智能体" value={selectedTeacherVersion ?? ''} disabled={busy}
|
||||
onChange={(event) => { const selected = teachers.find(item => item.version === Number(event.target.value)); setSelectedTeacherVersion(selected?.version); if (!topic && selected) setDefinition(selected.definition); }}
|
||||
className="mt-1 w-full rounded-lg border bg-background p-2 text-xs 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 || draftRevision) && <div className="px-5 pt-2">
|
||||
<button className="text-[11px] text-muted-foreground" onClick={() => setHistoryOpen(!historyOpen)} aria-expanded={historyOpen}>以往讨论 <ChevronDown className="inline h-3 w-3" /></button>
|
||||
{(historyOpen || draftRevision) && <div className="mt-2 flex gap-2">
|
||||
@@ -458,6 +461,13 @@ export function TeacherChatPanel({
|
||||
{topic?.unsaved && <div className="px-5 py-2 text-xs text-destructive">回复尚未保存,请复制或重试保存。<button className="ml-2 underline" onClick={() => void teacherApi.save(topicBase, topic.id).then(consume).catch(() => setError('保存失败,请先复制回复。'))}>重试保存</button></div>}
|
||||
{!topicEnabled && !error && !busy && <p className="px-5 pb-2 text-xs text-muted-foreground">{legacyFriend ? '这是旧版内置朋友的话题,仅供查看。请选择智能体新建话题。' : `${label}暂未开放,历史仍可查看。`}</p>}
|
||||
<form className="px-4 pb-4 pt-2" onSubmit={(e) => { e.preventDefault(); void send(); }}>
|
||||
{Boolean(unassignedDraft.text?.trim() || unassignedDraft.references?.length) && <details className="mb-2 text-xs text-muted-foreground">
|
||||
<summary>查看之前的项目草稿</summary>
|
||||
<div className="max-h-40 overflow-auto whitespace-pre-wrap rounded-lg border p-2">
|
||||
{unassignedDraft.text}
|
||||
{unassignedDraft.references?.map((ref, index) => <pre className="mt-2 whitespace-pre-wrap" key={index}>{ref.text}</pre>)}
|
||||
</div>
|
||||
</details>}
|
||||
{Boolean(legacyDraft.text?.trim() || legacyDraft.references?.length) && <details className="mb-2 text-xs text-muted-foreground">
|
||||
<summary>查看旧版朋友草稿</summary>
|
||||
<div className="max-h-40 overflow-auto whitespace-pre-wrap rounded-lg border p-2">
|
||||
|
||||
Reference in new issue
Block a user