|
|
@@ -5,7 +5,9 @@ import {
|
|
|
ArrowLeft, Send, Square, ChevronDown, ChevronRight,
|
|
|
Bot, User, Clock, Brain, Folder, Download, Trash2, X,
|
|
|
Save, RotateCcw, History, Key, BookMarked, LayoutDashboard,
|
|
|
+ MessageSquarePlus,
|
|
|
} from 'lucide-react'
|
|
|
+import toast from 'react-hot-toast'
|
|
|
import { agentsApi, type WorkspaceFile, type MemoryEntry, type CoreMemory, type RecallEntry } from '../api/agents'
|
|
|
import { knowledgeApi } from '../api/knowledge'
|
|
|
import { useAppStore } from '../store/app'
|
|
|
@@ -1173,6 +1175,51 @@ export default function Chat() {
|
|
|
const [continueFromPath, setContinueFromPath] = useState<string | null>(null)
|
|
|
const continueRunIdRef = useRef<string | null>(null)
|
|
|
const continuePathRef = useRef<string | null>(null)
|
|
|
+ // 会话记忆:当前对话 thread。每条消息带上,后端据此注入前情。
|
|
|
+ // 切换 agent 或点「新建会话」时清空,由本次 run 的 started 事件回填。
|
|
|
+ const threadIdRef = useRef<string | null>(null)
|
|
|
+ const [historyOpen, setHistoryOpen] = useState(false)
|
|
|
+
|
|
|
+ // 切换 agent → 重置 thread(不同 agent 的会话不混)
|
|
|
+ useEffect(() => { threadIdRef.current = null }, [agentId])
|
|
|
+
|
|
|
+ // 历史会话列表(下拉打开时拉取)
|
|
|
+ const { data: threadsData, refetch: refetchThreads } = useQuery({
|
|
|
+ queryKey: ['threads', agentId],
|
|
|
+ queryFn: () => agentsApi.threads(agentId!),
|
|
|
+ enabled: !!agentId && historyOpen,
|
|
|
+ })
|
|
|
+
|
|
|
+ // 新建会话:清空 thread + 清空消息(产物 continue 上下文也一并清)
|
|
|
+ const startNewThread = useCallback(() => {
|
|
|
+ threadIdRef.current = null
|
|
|
+ continueRunIdRef.current = null
|
|
|
+ continuePathRef.current = null
|
|
|
+ setContinueFromRunId(null)
|
|
|
+ setContinueFromPath(null)
|
|
|
+ setMessages([])
|
|
|
+ setHistoryOpen(false)
|
|
|
+ }, [])
|
|
|
+
|
|
|
+ // 载入一条历史会话:拉 thread 内 runs 渲染成气泡,并接上该 thread
|
|
|
+ const loadThread = useCallback(async (threadId: string) => {
|
|
|
+ if (!agentId) return
|
|
|
+ try {
|
|
|
+ const res = await agentsApi.threadRuns(agentId, threadId)
|
|
|
+ const msgs: Message[] = []
|
|
|
+ for (const r of res.runs) {
|
|
|
+ msgs.push({ id: r.id + '_u', role: 'user', text: r.input, status: 'done', steps: [], turns: [] })
|
|
|
+ msgs.push({ id: r.id + '_a', role: 'assistant', text: r.output || '(无输出)',
|
|
|
+ status: r.status === 'failed' ? 'error' : 'done',
|
|
|
+ runId: r.id, workspacePath: r.workspace_path || undefined, steps: [], turns: [] })
|
|
|
+ }
|
|
|
+ setMessages(msgs)
|
|
|
+ threadIdRef.current = threadId
|
|
|
+ setHistoryOpen(false)
|
|
|
+ } catch (e) {
|
|
|
+ toast.error('载入会话失败')
|
|
|
+ }
|
|
|
+ }, [agentId])
|
|
|
|
|
|
// Mode selector for continue-this-run flows.
|
|
|
// iterate (default): full pipeline, new cycle_N+1
|
|
|
@@ -1267,6 +1314,8 @@ export default function Chat() {
|
|
|
// Only meaningful when continuing — fresh runs always run as iterate
|
|
|
mode: ctxRunId ? ctxMode : 'iterate',
|
|
|
target_subagent: ctxRunId && ctxMode === 'edit' ? ctxTarget : '',
|
|
|
+ // 会话记忆:带上当前 thread(首条消息为空 → 后端新建,started 回填)
|
|
|
+ thread_id: threadIdRef.current ?? '',
|
|
|
}),
|
|
|
signal: abortCtrl.signal,
|
|
|
})
|
|
|
@@ -1352,6 +1401,9 @@ export default function Chat() {
|
|
|
m.id === assistantId ? { ...m, runId: rid } : m,
|
|
|
))
|
|
|
}
|
|
|
+ // 会话记忆:回填后端解析/新建的 thread_id,后续消息复用
|
|
|
+ const tid = payload.thread_id as string | undefined
|
|
|
+ if (tid) threadIdRef.current = tid
|
|
|
|
|
|
} else if (eventType === 'answer') {
|
|
|
// answer event: backend sends {content, step, tool, duration_ms}
|
|
|
@@ -1605,6 +1657,49 @@ export default function Chat() {
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
+ {/* 会话记忆:新建会话 + 历史会话 */}
|
|
|
+ <button
|
|
|
+ onClick={startNewThread}
|
|
|
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium bg-gray-100 text-gray-600 hover:bg-gray-200 transition-colors"
|
|
|
+ title="开始一段新对话(不带前情记忆)"
|
|
|
+ >
|
|
|
+ <MessageSquarePlus size={13} />
|
|
|
+ 新会话
|
|
|
+ </button>
|
|
|
+ <div className="relative">
|
|
|
+ <button
|
|
|
+ onClick={() => { setHistoryOpen(o => !o); if (!historyOpen) refetchThreads() }}
|
|
|
+ className={clsx(
|
|
|
+ 'flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
|
|
|
+ historyOpen ? 'bg-indigo-100 text-indigo-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200',
|
|
|
+ )}
|
|
|
+ title="历史会话"
|
|
|
+ >
|
|
|
+ <History size={13} />
|
|
|
+ 历史
|
|
|
+ </button>
|
|
|
+ {historyOpen && (
|
|
|
+ <div className="absolute right-0 top-full mt-1 w-72 max-h-80 overflow-y-auto bg-white border border-gray-200 rounded-xl shadow-lg z-30">
|
|
|
+ {(threadsData?.threads ?? []).length === 0 ? (
|
|
|
+ <p className="text-xs text-gray-400 px-4 py-3">暂无历史会话</p>
|
|
|
+ ) : (
|
|
|
+ (threadsData?.threads ?? []).map(t => (
|
|
|
+ <button
|
|
|
+ key={t.id}
|
|
|
+ onClick={() => loadThread(t.id)}
|
|
|
+ className={clsx(
|
|
|
+ 'w-full text-left px-4 py-2.5 hover:bg-gray-50 border-b border-gray-50 last:border-0',
|
|
|
+ threadIdRef.current === t.id && 'bg-indigo-50',
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <p className="text-xs font-medium text-gray-800 truncate">{t.title || '未命名会话'}</p>
|
|
|
+ <p className="text-[10px] text-gray-400 mt-0.5">{t.run_count} 条消息</p>
|
|
|
+ </button>
|
|
|
+ ))
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
{/* Workspace link */}
|
|
|
<button
|
|
|
onClick={() => navigate(`/agents/${agentId}/workspace`)}
|