Skip to content

Commit 66087d9

Browse files
committed
feat(chat): add temporary chat mode
1 parent bf422bf commit 66087d9

4 files changed

Lines changed: 51 additions & 18 deletions

File tree

apps/api/routers/chat.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,30 @@ def query(payload: ChatQuery, request: Request, user: dict = Depends(get_current
2727
version = db.one("SELECT * FROM agent_versions WHERE id=?", [agent["default_version_id"]])
2828
if not version:
2929
raise HTTPException(status_code=404, detail="Agent version not found")
30-
session_id = payload.session_id
30+
temporary_chat = bool((payload.context or {}).get("temporary_chat"))
31+
session_id = None if temporary_chat else payload.session_id
3132
if session_id:
3233
existing_session = db.one("SELECT * FROM sessions WHERE id=?", [session_id])
3334
if not existing_session or existing_session["user_id"] != user["id"]:
3435
raise HTTPException(status_code=403, detail="No permission to use this session")
3536
if existing_session.get("agent_id") and existing_session["agent_id"] != agent["id"]:
3637
raise HTTPException(status_code=400, detail="Session is bound to a different agent")
37-
else:
38+
elif not temporary_chat:
3839
session_id = db.new_id("session")
3940
t = db.now()
4041
db.insert("sessions", {"id": session_id, "user_id": user["id"], "agent_id": agent["id"], "title": payload.message[:30], "status": "active", "created_at": t, "updated_at": t})
41-
db.insert("messages", {"id": db.new_id("msg"), "session_id": session_id, "role": "user", "content": payload.message, "content_type": "text", "created_at": db.now()})
42+
if not temporary_chat:
43+
db.insert("messages", {"id": db.new_id("msg"), "session_id": session_id, "role": "user", "content": payload.message, "content_type": "text", "created_at": db.now()})
4244
trace_id = trace_service.create_trace(user["id"], payload.message, agent_id=agent["id"], session_id=session_id, agent_version=version["version"], request_id=getattr(request.state, "request_id", ""))
4345
start = time.time()
4446
try:
4547
output = adapters.call_adapter(agent, version, payload.message, trace_id, payload.context, user=user)
4648
duration = int((time.time() - start) * 1000)
4749
trace_service.finish_trace(trace_id, output, "success", duration)
48-
db.insert("messages", {"id": db.new_id("msg"), "session_id": session_id, "role": "assistant", "content": json.dumps(output, ensure_ascii=False), "content_type": "agent_result", "created_at": db.now()})
49-
db.update("sessions", "id", session_id, {"updated_at": db.now()})
50-
audit("chat_query", user, "agent", agent["id"], {"trace_id": trace_id, "message": payload.message}, request)
50+
if not temporary_chat:
51+
db.insert("messages", {"id": db.new_id("msg"), "session_id": session_id, "role": "assistant", "content": json.dumps(output, ensure_ascii=False), "content_type": "agent_result", "created_at": db.now()})
52+
db.update("sessions", "id", session_id, {"updated_at": db.now()})
53+
audit("chat_query", user, "agent", agent["id"], {"trace_id": trace_id, "message": payload.message, "temporary_chat": temporary_chat}, request)
5154
return {"session_id": session_id, "trace_id": trace_id, "result": output}
5255
except Exception as exc:
5356
duration = int((time.time() - start) * 1000)

apps/web/static/app.js

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let sessionSearchCache = {};
1717
let answerDraftCache = {};
1818
let activeSessionId = '';
1919
let activeChatMessages = [];
20+
let temporaryChat = false;
2021
let chatCanvasDraft = '';
2122
let chatCanvasVersions = [];
2223
let chatCanvasVersionIndex = -1;
@@ -1666,7 +1667,7 @@ function renderChat(){
16661667
${renderContextPackPanel()}
16671668
</aside>
16681669
<section class="chat-stage">
1669-
<div class="chat-stage-head"><div><span>Data Agent</span><b id="chatSessionTitle">新对话</b></div><div class="chat-stage-actions"><div class="tool-strip" id="toolMode"><button class="active" data-mode="auto" onclick="setToolMode('auto',this)">自动</button><button data-mode="analysis" onclick="setToolMode('analysis',this)">分析</button><button data-mode="sql" onclick="setToolMode('sql',this)">SQL</button><button data-mode="codex" onclick="setToolMode('codex',this)">Codex</button></div><div class="brief-strip" aria-label="会话 Brief 操作"><button onclick="writeChatBriefToCanvas(this)">生成 Brief</button><button onclick="copyChatBrief(this)">复制 Brief</button><button onclick="downloadChatBriefMarkdown(this)">下载 Brief</button></div></div></div>
1670+
<div class="chat-stage-head"><div><span>Data Agent</span><b id="chatSessionTitle">${temporaryChat?'临时对话':'新对话'}</b></div><div class="chat-stage-actions"><div class="tool-strip" id="toolMode"><button class="active" data-mode="auto" onclick="setToolMode('auto',this)">自动</button><button data-mode="analysis" onclick="setToolMode('analysis',this)">分析</button><button data-mode="sql" onclick="setToolMode('sql',this)">SQL</button><button data-mode="codex" onclick="setToolMode('codex',this)">Codex</button></div><div class="session-mode-strip" aria-label="会话保存模式"><button id="temporaryChatButton" class="${temporaryChat?'active':''}" aria-pressed="${temporaryChat}" onclick="toggleTemporaryChat()">临时对话</button></div><div class="brief-strip" aria-label="会话 Brief 操作"><button onclick="writeChatBriefToCanvas(this)">生成 Brief</button><button onclick="copyChatBrief(this)">复制 Brief</button><button onclick="downloadChatBriefMarkdown(this)">下载 Brief</button></div></div></div>
16701671
<div id="chatMessages" class="chat-thread">${chatEmptyState()}</div>
16711672
<div class="composer-card">
16721673
<div id="chatContextBar">${renderChatContextBar()}</div>
@@ -1696,6 +1697,7 @@ function renderChat(){
16961697
</div>`;
16971698
const router=agents.find(a=>a.id==='agent_router'); if(router) document.getElementById('chatAgent').value=router.id;
16981699
syncChatContextBar();
1700+
syncTemporaryChatControls();
16991701
syncChatBriefControls();
17001702
refreshChatSessions().catch(()=>{document.getElementById('chatSessionList').innerHTML=emptyState('会话读取失败','仍可直接开始新对话。')});
17011703
}
@@ -1734,12 +1736,28 @@ function renderChatContextBar(){
17341736
${chatContextChip('工具',toolLabels[ctx.toolMode]||ctx.toolMode,'会话模式')}
17351737
${chatContextChip('证据',depthLabels[ctx.evidenceDepth]||ctx.evidenceDepth,'Trace')}
17361738
${chatContextChip('工作包',contextPackSummaryLabel(),contextPackSummaryDetail(),contextPackHasContent()?'accent':'')}
1739+
${chatContextChip('历史',temporaryChat?'临时对话':'保存会话',temporaryChat?'不写入会话库 · 保留 Trace':'可恢复 · 可搜索',temporaryChat?'accent':'')}
17371740
</div>`;
17381741
}
17391742
function syncChatContextBar(){
17401743
const box=document.getElementById('chatContextBar');
17411744
if(box) box.innerHTML=renderChatContextBar();
17421745
}
1746+
function syncTemporaryChatControls(){
1747+
const button=document.getElementById('temporaryChatButton');
1748+
if(button){
1749+
button.classList.toggle('active',temporaryChat);
1750+
button.setAttribute('aria-pressed',String(temporaryChat));
1751+
button.title=temporaryChat?'临时对话不会写入会话库或消息历史,Trace 和审计仍会保留。':'开启临时对话';
1752+
}
1753+
const title=document.getElementById('chatSessionTitle');
1754+
if(title&&!activeSessionId&&temporaryChat) title.innerText='临时对话';
1755+
}
1756+
function toggleTemporaryChat(){
1757+
temporaryChat=!temporaryChat;
1758+
startNewChat({preserveTemporary:true});
1759+
toast(temporaryChat?'临时对话已开启:不会写入会话库,Trace 和审计仍保留':'临时对话已关闭:后续提问会保存为会话');
1760+
}
17431761
function composerContextItems(){
17441762
const q=(composerContextFilterState.q||'').trim().toLowerCase();
17451763
const type=composerContextFilterState.type||'all';
@@ -2744,7 +2762,8 @@ function selectedChatContext(){
27442762
const base={
27452763
dataset_id:document.getElementById('chatDataset')?.value||null,
27462764
tool_mode:document.querySelector('#toolMode button.active')?.dataset.mode||'auto',
2747-
evidence_depth:document.getElementById('traceDepth')?.value||'standard'
2765+
evidence_depth:document.getElementById('traceDepth')?.value||'standard',
2766+
temporary_chat:temporaryChat
27482767
};
27492768
if(contextPackHasContent()) base.context_pack=contextPackPayload();
27502769
return base;
@@ -2947,17 +2966,20 @@ function bindSessionToContextPack(id,btn){
29472966
setBusy(btn,false);
29482967
}
29492968
}
2950-
function startNewChat(){
2969+
function startNewChat(opts={}){
2970+
if(!opts.preserveTemporary) temporaryChat=false;
29512971
activeSessionId='';
29522972
activeChatMessages=[];
29532973
currentTrace=null;
29542974
chatCanvasDraft='';
29552975
chatCanvasVersions=[];
29562976
chatCanvasVersionIndex=-1;
2957-
const title=document.getElementById('chatSessionTitle'); if(title) title.innerText='新对话';
2977+
const title=document.getElementById('chatSessionTitle'); if(title) title.innerText=temporaryChat?'临时对话':'新对话';
29582978
const box=document.getElementById('chatMessages'); if(box) box.innerHTML=chatEmptyState();
29592979
const trace=document.getElementById('traceBox'); if(trace) trace.innerHTML=emptyState('暂无 Trace','发送问题后会显示执行状态、SQL、工具调用与步骤输出。');
29602980
setChatCanvasDraft('','Canvas 已清空',{record:false});
2981+
syncTemporaryChatControls();
2982+
syncChatContextBar();
29612983
syncChatBriefControls();
29622984
renderSessionList();
29632985
requestAnimationFrame(()=>document.getElementById('chatInput')?.focus());
@@ -2971,9 +2993,11 @@ async function loadChatSession(id){
29712993
try{
29722994
const session=await api('/api/sessions/'+id);
29732995
sessionSearchCache[session.id]=buildSessionSearchIndex(session);
2996+
temporaryChat=false;
29742997
activeSessionId=session.id;
29752998
const title=document.getElementById('chatSessionTitle'); if(title) title.innerText=session.title||session.id;
29762999
const agentSelect=document.getElementById('chatAgent'); if(agentSelect&&session.agent_id) agentSelect.value=session.agent_id;
3000+
syncTemporaryChatControls();
29773001
syncChatContextBar();
29783002
const messages=session.messages||[];
29793003
activeChatMessages=messages;
@@ -3205,6 +3229,7 @@ function stopChatGeneration(btn){
32053229
async function sendChat(btn){
32063230
if(chatSending){ stopChatGeneration(); return; }
32073231
const input=document.getElementById('chatInput'); const msg=input.value.trim(); if(!msg) return; const agent_id=document.getElementById('chatAgent').value;
3232+
const isTemporary=temporaryChat;
32083233
chatSending=true;
32093234
chatAbortController=new AbortController();
32103235
const sendButton=document.getElementById('chatSendButton')||document.querySelector('#page-chat .composer-row button');
@@ -3219,17 +3244,18 @@ async function sendChat(btn){
32193244
const pendingId='pending-'+Date.now();
32203245
box.innerHTML+=`<div id="${pendingId}" class="message assistant pending-message">${inlineLoading('Agent 正在路由和生成答案')}</div>`;
32213246
try{
3222-
const data=await api('/api/chat/query',{method:'POST',signal:chatAbortController.signal,body:JSON.stringify({message:msg,agent_id,session_id:activeSessionId||null,context:selectedChatContext()})}); const r=data.result||{};
3223-
activeSessionId=data.session_id||activeSessionId;
3224-
const title=document.getElementById('chatSessionTitle'); if(title) title.innerText=sessionTitle({title:msg});
3247+
const data=await api('/api/chat/query',{method:'POST',signal:chatAbortController.signal,body:JSON.stringify({message:msg,agent_id,session_id:isTemporary?null:(activeSessionId||null),context:selectedChatContext()})}); const r=data.result||{};
3248+
activeSessionId=isTemporary?'':(data.session_id||activeSessionId);
3249+
const title=document.getElementById('chatSessionTitle'); if(title) title.innerText=isTemporary?'临时对话':sessionTitle({title:msg});
32253250
document.getElementById(pendingId)?.remove();
3226-
box.innerHTML+=`<div class="message assistant rich-message">${resultHtml(r,{session_id:activeSessionId,trace_id:data.trace_id,question:msg})}</div>`;
3251+
box.innerHTML+=`<div class="message assistant rich-message">${resultHtml(r,{session_id:isTemporary?'':activeSessionId,trace_id:data.trace_id,question:msg})}</div>`;
32273252
activeChatMessages.push({role:'assistant',content:JSON.stringify(r),content_type:'agent_result',created_at:new Date().toISOString()});
3228-
if(activeSessionId) sessionSearchCache[activeSessionId]=buildSessionSearchIndex({messages:activeChatMessages});
3253+
if(!isTemporary&&activeSessionId) sessionSearchCache[activeSessionId]=buildSessionSearchIndex({messages:activeChatMessages});
32293254
syncChatBriefControls();
32303255
await openTrace(data.trace_id);
32313256
if(!chatCanvasValue().trim()) setChatCanvasDraft(chatCanvasTemplate('report'),'已根据本轮回答生成报告要点');
3232-
await refreshChatSessions();
3257+
if(isTemporary) renderSessionList();
3258+
else await refreshChatSessions();
32333259
}catch(e){
32343260
const stopped=e?.name==='AbortError';
32353261
const banner=stopped

apps/web/static/styles.css

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@ button.ghost,.prompt-pill{background:#fff;color:#2d2d2a;border-color:#e1ded6}
676676
.tool-strip{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}
677677
.tool-strip button{background:#fff;color:#3d3a34;border:1px solid var(--line);border-radius:999px;padding:7px 11px;box-shadow:none}
678678
.tool-strip button.active{background:#171717;color:#fff;border-color:#171717}
679+
.session-mode-strip{display:flex;justify-content:flex-end}
680+
.session-mode-strip button{background:#fff7d6;color:#5f4b00;border:1px solid #ead89d;border-radius:999px;padding:6px 10px;box-shadow:none;font-size:11px}
681+
.session-mode-strip button.active{background:#171717;color:#fff;border-color:#171717}
679682
.brief-strip{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}
680683
.brief-strip button{background:#f6f5ef;color:#3d3a34;border:1px solid var(--line);border-radius:999px;padding:6px 9px;box-shadow:none;font-size:11px}
681684
.brief-strip button:hover{background:#171717;color:#fff;border-color:#171717;filter:none;transform:none}
@@ -686,7 +689,7 @@ button.ghost,.prompt-pill{background:#fff;color:#2d2d2a;border-color:#e1ded6}
686689
.chat-welcome p{margin:0;max-width:520px;color:#6b6b62;line-height:1.6}
687690
.assistant-mark{display:grid;place-items:center;width:44px;height:44px;border-radius:14px;background:#171717;color:#fff;font-weight:900}
688691
.composer-card{border-top:1px solid var(--line);padding:12px 14px;background:#fbfaf7}
689-
.chat-context-bar{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:10px}
692+
.chat-context-bar{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin-bottom:10px}
690693
.chat-context-chip{display:grid;gap:3px;min-width:0;background:#fff;border:1px solid var(--line);border-radius:14px;padding:9px 10px;color:#3d3a34}
691694
.chat-context-chip.primary{background:#171717;color:#fff;border-color:#171717}
692695
.chat-context-chip.accent{background:#eef8f3;border-color:#cfe8dc;color:#0d6d58}
@@ -798,6 +801,7 @@ button.ghost,.prompt-pill{background:#fff;color:#2d2d2a;border-color:#e1ded6}
798801
.context-card{border-left:0;border-top:1px solid var(--line);padding-left:0;padding-top:12px}
799802
.chat-stage-head{display:grid;gap:10px}
800803
.tool-strip{justify-content:flex-start}
804+
.session-mode-strip{justify-content:flex-start}
801805
.chat-thread{padding:14px}
802806
.chat-welcome h2{font-size:24px}
803807
.chat-context-bar{grid-template-columns:1fr}

docs/ui-function-map.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
1. **控制塔**:总体状态、Agent/Trace/SQL/Codex 指标、关键链路、快捷分析入口、任务 composer、最近会话/报告继续工作面板和最近任务队列 Trace 下钻。
66
2. **Agent Studio**:按能力目录选择 Agent,支持名称/ID/Adapter 搜索、能力类型和风险筛选,右侧 inspector 查看风险、版本、知识绑定,并直接进入问数/研究。
7-
3. **智能问数**:左侧场景问题、可按标题和消息内容搜索/重命名/归档/项目筛选的会话库和 Project-style 工作包,中间对话与图表/表格/报告,右侧 Trace 证据链和可编辑工作 Canvas;composer 显示 Agent/数据集/工具/证据/工作包上下文,并支持 `+` 添加上下文菜单快速加入数据集、知识库、报告、切换记忆边界、带 Canvas 提问和捕获/应用工作包,支持 `/` 工具菜单直达指标解释、图表方案、SQL 草稿、Canvas 大纲、报告要点、工作包应用/捕获、深度研究、Codex 任务和数据资产。工作包支持搜索并加入数据集、知识库和报告资产,可逐项打开或移除上下文,可加入多个来源会话并在会话库中只看项目会话,可保存/加载/删除本地项目预设,可设置项目内/默认记忆边界,可把多来源会话随 `context_pack.session_ids` 提交给问数请求,可把 Canvas 草稿随 `context_pack.canvas_markdown` 提交给问数请求,可把知识库随 `context_pack.knowledge_base_ids` 一起提交给问数请求,并提供解释指标、生成图表、打开数据资产、转深度研究、创建 Codex 任务、发送中停止生成、用户问题编辑/重问/分支/写入 Canvas/复制、会话 Brief 生成/复制/下载、回答写入/追加 Canvas、提取下一步、回答后续追问建议、捕获上下文、应用工作包、生成分析大纲/SQL 草稿/报告要点、Canvas Markdown 加粗/斜体/标题/项目列表/编号列表格式化、Canvas 选区润色/补证据/压缩/展开/追问、本地版本回退、版本变化对比、保存报告草稿和下载 Markdown 的快捷动作。
7+
3. **智能问数**:左侧场景问题、可按标题和消息内容搜索/重命名/归档/项目筛选的会话库和 Project-style 工作包,中间对话与图表/表格/报告,右侧 Trace 证据链和可编辑工作 Canvas;composer 显示 Agent/数据集/工具/证据/工作包上下文,并支持临时对话模式(不写入会话库或消息历史,但保留 Trace 和 audit)、`+` 添加上下文菜单快速加入数据集、知识库、报告、切换记忆边界、带 Canvas 提问和捕获/应用工作包,支持 `/` 工具菜单直达指标解释、图表方案、SQL 草稿、Canvas 大纲、报告要点、工作包应用/捕获、深度研究、Codex 任务和数据资产。工作包支持搜索并加入数据集、知识库和报告资产,可逐项打开或移除上下文,可加入多个来源会话并在会话库中只看项目会话,可保存/加载/删除本地项目预设,可设置项目内/默认记忆边界,可把多来源会话随 `context_pack.session_ids` 提交给问数请求,可把 Canvas 草稿随 `context_pack.canvas_markdown` 提交给问数请求,可把知识库随 `context_pack.knowledge_base_ids` 一起提交给问数请求,并提供解释指标、生成图表、打开数据资产、转深度研究、创建 Codex 任务、发送中停止生成、用户问题编辑/重问/分支/写入 Canvas/复制、会话 Brief 生成/复制/下载、回答写入/追加 Canvas、提取下一步、回答后续追问建议、捕获上下文、应用工作包、生成分析大纲/SQL 草稿/报告要点、Canvas Markdown 加粗/斜体/标题/项目列表/编号列表格式化、Canvas 选区润色/补证据/压缩/展开/追问、本地版本回退、版本变化对比、保存报告草稿和下载 Markdown 的快捷动作。
88
4. **深度研究**:研究 brief、可审批计划、历史研究任务队列、证据 Trace、报告草稿入口和取消/刷新任务。
99
5. **分析面板**:物化经营总览面板、创建面板 brief、添加 Widget,并通过 SQL Guard 预校验后刷新面板证据。
1010
6. **数据能力**:可搜索/筛选的数据资产库、数据集详情、字段/指标、只读 SQL Workbench、数据画像、业务规则运行、质量历史复核,以及从数据集直接进入问数/研究/Codex 治理任务的上下文动作。

0 commit comments

Comments
 (0)