u
This commit is contained in:
@@ -144,43 +144,7 @@ export const vfs = {
|
|||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/vfs/drafts/response
|
|
||||||
* Send response to virtual draft request (action: read or list).
|
|
||||||
* @param {{ requestId: string, content?: object|string, files?: Array<object>, error?: string }} body
|
|
||||||
*/
|
|
||||||
// #region POST /api/vfs/drafts/response — respondDraft
|
|
||||||
async respondDraft(body) {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/vfs/drafts/response', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...clientIdHeaders(),
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
let errText = '';
|
|
||||||
try {
|
|
||||||
const errData = await res.json();
|
|
||||||
errText = errData.error || errData.message;
|
|
||||||
} catch {
|
|
||||||
errText = await res.text().catch(() => '');
|
|
||||||
}
|
|
||||||
console.error('[vfs] respondDraft failed:', res.status, errText);
|
|
||||||
return { ok: false, error: errText };
|
|
||||||
}
|
|
||||||
if (res.status === 204) return { ok: true };
|
|
||||||
const text = await res.text();
|
|
||||||
return text ? JSON.parse(text) : { ok: true };
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[vfs] respondDraft error:', err);
|
|
||||||
return { ok: false, error: err.message };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/vfs/skills/restore
|
* POST /api/vfs/skills/restore
|
||||||
|
|||||||
@@ -285,6 +285,67 @@ function closeConversationStream() {
|
|||||||
}
|
}
|
||||||
activeStreamConversationId = null;
|
activeStreamConversationId = null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Normalizes tool arguments to detect a virtual draft read (drafts/delivery, drafts/quote, drafts/order).
|
||||||
|
* @param {object} args
|
||||||
|
* @returns {{ filename: string, draftKey: string } | null}
|
||||||
|
*/
|
||||||
|
export function getDraftInfoFromToolArgs(args) {
|
||||||
|
if (!args || typeof args !== 'object') return null;
|
||||||
|
const raw = args.filename || args.path || args.file || args.name || '';
|
||||||
|
if (typeof raw !== 'string' || !raw.trim()) return null;
|
||||||
|
const normalized = raw.trim().replace(/^\/?(api\/)?vfs\//, '');
|
||||||
|
if (normalized.startsWith('drafts/')) {
|
||||||
|
const draftKey = normalized.replace(/^drafts\//, '').replace(/\.json$/, '').toLowerCase();
|
||||||
|
return {
|
||||||
|
filename: normalized,
|
||||||
|
draftKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a tool call or stream tool event represents an interactive tool
|
||||||
|
* that awaits user intervention (ask_user or virtual draft read).
|
||||||
|
* @param {object} item
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isInteractiveTool(item) {
|
||||||
|
if (!item) return false;
|
||||||
|
const name = item.name || item.function?.name;
|
||||||
|
if (name === 'ask_user') return true;
|
||||||
|
if (name === 'read_file') {
|
||||||
|
let args = {};
|
||||||
|
const rawContent = item.content !== undefined ? item.content : (item.arguments || item.parameters || item.function?.arguments);
|
||||||
|
if (typeof rawContent === 'string') {
|
||||||
|
try {
|
||||||
|
args = JSON.parse(rawContent);
|
||||||
|
} catch {
|
||||||
|
if (rawContent.includes('drafts/')) {
|
||||||
|
args = { filename: rawContent };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (rawContent && typeof rawContent === 'object') {
|
||||||
|
args = rawContent;
|
||||||
|
}
|
||||||
|
const secondaryRaw = item.arguments || item.function?.arguments;
|
||||||
|
if ((!args.filename && !args.path && !args.file) && secondaryRaw && secondaryRaw !== rawContent) {
|
||||||
|
try {
|
||||||
|
const parsed = typeof secondaryRaw === 'string' ? JSON.parse(secondaryRaw) : secondaryRaw;
|
||||||
|
if (parsed && typeof parsed === 'object') {
|
||||||
|
args = { ...args, ...parsed };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (typeof secondaryRaw === 'string' && secondaryRaw.includes('drafts/')) {
|
||||||
|
args = { filename: secondaryRaw };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Boolean(getDraftInfoFromToolArgs(args));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
/**
|
/**
|
||||||
@@ -518,13 +579,18 @@ const actions = {
|
|||||||
|
|
||||||
const messages = msgsRes?.messages || [];
|
const messages = msgsRes?.messages || [];
|
||||||
const updatedConv = convRes?.conversation;
|
const updatedConv = convRes?.conversation;
|
||||||
|
const hasPendingInteractive = Boolean(
|
||||||
|
selectPendingAskUser({ messages }) || selectPendingDraftRead({ messages })
|
||||||
|
);
|
||||||
|
|
||||||
chatAutomat.setState((prev) => ({
|
chatAutomat.setState((prev) => ({
|
||||||
messages,
|
messages,
|
||||||
loadingMessages: false,
|
loadingMessages: false,
|
||||||
conversationState: updatedConv?.state || prev.conversationState,
|
conversationState: hasPendingInteractive ? 'idle' : (updatedConv?.state || prev.conversationState),
|
||||||
|
streamPhase: hasPendingInteractive ? 'idle' : prev.streamPhase,
|
||||||
|
isSending: hasPendingInteractive ? false : prev.isSending,
|
||||||
conversations: updatedConv
|
conversations: updatedConv
|
||||||
? prev.conversations.map((c) => (c.id === updatedConv.id ? updatedConv : c))
|
? prev.conversations.map((c) => (c.id === updatedConv.id ? (hasPendingInteractive ? { ...updatedConv, state: 'idle' } : updatedConv) : c))
|
||||||
: prev.conversations,
|
: prev.conversations,
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -881,7 +947,7 @@ const actions = {
|
|||||||
const updatedTool = {
|
const updatedTool = {
|
||||||
id: data.id || `tool-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
id: data.id || `tool-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||||
state: 'green',
|
state: 'green',
|
||||||
name: data.name || '',
|
name: data.name || toolName || (toolIdx >= 0 ? tools[toolIdx].name : '') || '',
|
||||||
arguments: data.arguments,
|
arguments: data.arguments,
|
||||||
content: data.content !== undefined ? data.content : data.arguments || '',
|
content: data.content !== undefined ? data.content : data.arguments || '',
|
||||||
result: data.result,
|
result: data.result,
|
||||||
@@ -895,12 +961,19 @@ const actions = {
|
|||||||
lastTurn.tools = tools;
|
lastTurn.tools = tools;
|
||||||
turns[lastIdx] = lastTurn;
|
turns[lastIdx] = lastTurn;
|
||||||
|
|
||||||
const hasAskUser = data.name === 'ask_user';
|
const isInteractive = isInteractiveTool(data) || isInteractiveTool(updatedTool);
|
||||||
|
const nextConversations = isInteractive
|
||||||
|
? prev.conversations.map((c) =>
|
||||||
|
c.id === prev.selectedConversationId ? { ...c, state: 'idle' } : c
|
||||||
|
)
|
||||||
|
: prev.conversations;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTurns: turns,
|
activeTurns: turns,
|
||||||
streamPhase: hasAskUser ? 'idle' : 'working',
|
streamPhase: isInteractive ? 'idle' : 'working',
|
||||||
conversationState: hasAskUser ? 'idle' : prev.conversationState,
|
conversationState: isInteractive ? 'idle' : prev.conversationState,
|
||||||
isSending: hasAskUser ? false : prev.isSending,
|
isSending: isInteractive ? false : prev.isSending,
|
||||||
|
conversations: nextConversations,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1197,10 +1270,10 @@ const actions = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit a tool response (e.g. for ask_user) via POST /api/modelconversation/:id/message.
|
* Submit a tool response (e.g. for ask_user) via POST /api/modelconversation/:id/message.
|
||||||
* Body: { role: 'tool', tool_call_id, content, answer }
|
* Body: { role: 'tool', tool_call_id, name, content, answer }
|
||||||
* @param {{ tool_call_id: string, answer: string }} params
|
* @param {{ tool_call_id: string, answer?: string, name?: string, content?: any }} params
|
||||||
*/
|
*/
|
||||||
async sendToolResponse({ tool_call_id, answer }) {
|
async sendToolResponse({ tool_call_id, answer, name = 'ask_user', content = undefined }) {
|
||||||
const { selectedConversationId, isSending } = chatAutomat.state;
|
const { selectedConversationId, isSending } = chatAutomat.state;
|
||||||
if (!selectedConversationId) {
|
if (!selectedConversationId) {
|
||||||
throw new Error('Keine Unterhaltung ausgewählt');
|
throw new Error('Keine Unterhaltung ausgewählt');
|
||||||
@@ -1209,12 +1282,13 @@ const actions = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolContent = content !== undefined ? content : answer;
|
||||||
const payload = {
|
const payload = {
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
tool_call_id,
|
tool_call_id,
|
||||||
name: 'ask_user',
|
name,
|
||||||
content: answer,
|
content: toolContent,
|
||||||
answer,
|
...(answer !== undefined ? { answer } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const tempId = `temp-${Date.now()}`;
|
const tempId = `temp-${Date.now()}`;
|
||||||
@@ -1258,7 +1332,7 @@ const actions = {
|
|||||||
if (m.role === 'assistant' && !m.turns) {
|
if (m.role === 'assistant' && !m.turns) {
|
||||||
try {
|
try {
|
||||||
const p = typeof m.message === 'string' ? JSON.parse(m.message) : m.message;
|
const p = typeof m.message === 'string' ? JSON.parse(m.message) : m.message;
|
||||||
if (p?.tool_calls?.some((tc) => tc.id === tool_call_id || tc.function?.name === 'ask_user')) {
|
if (p?.tool_calls?.some((tc) => tc.id === tool_call_id || tc.function?.name === name || tc.function?.name === 'ask_user')) {
|
||||||
foundExisting = true;
|
foundExisting = true;
|
||||||
return { ...m, turns: sealedTurns };
|
return { ...m, turns: sealedTurns };
|
||||||
}
|
}
|
||||||
@@ -1367,12 +1441,12 @@ const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAssistant = message.role === 'assistant' || message.role === 'model';
|
const isAssistant = message.role === 'assistant' || message.role === 'model';
|
||||||
let hasAskUser = false;
|
let hasInteractiveTool = false;
|
||||||
if (isAssistant) {
|
if (isAssistant) {
|
||||||
try {
|
try {
|
||||||
const payload = typeof message.message === 'string' ? JSON.parse(message.message) : message.message;
|
const payload = typeof message.message === 'string' ? JSON.parse(message.message) : message.message;
|
||||||
if (payload?.tool_calls?.some((tc) => tc.function?.name === 'ask_user')) {
|
if (payload?.tool_calls?.some((tc) => isInteractiveTool(tc))) {
|
||||||
hasAskUser = true;
|
hasInteractiveTool = true;
|
||||||
}
|
}
|
||||||
if (Array.isArray(payload?.tool_calls)) {
|
if (Array.isArray(payload?.tool_calls)) {
|
||||||
for (const tc of payload.tool_calls) {
|
for (const tc of payload.tool_calls) {
|
||||||
@@ -1411,9 +1485,9 @@ const actions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextConversationState = hasAskUser ? 'idle' : prev.conversationState;
|
const nextConversationState = hasInteractiveTool ? 'idle' : prev.conversationState;
|
||||||
const nextStreamPhase = hasAskUser ? 'idle' : prev.streamPhase;
|
const nextStreamPhase = hasInteractiveTool ? 'idle' : prev.streamPhase;
|
||||||
const nextConversations = hasAskUser
|
const nextConversations = hasInteractiveTool
|
||||||
? prev.conversations.map((c) =>
|
? prev.conversations.map((c) =>
|
||||||
c.id === prev.selectedConversationId ? { ...c, state: 'idle' } : c
|
c.id === prev.selectedConversationId ? { ...c, state: 'idle' } : c
|
||||||
)
|
)
|
||||||
@@ -1425,6 +1499,7 @@ const actions = {
|
|||||||
conversationState: nextConversationState,
|
conversationState: nextConversationState,
|
||||||
streamPhase: nextStreamPhase,
|
streamPhase: nextStreamPhase,
|
||||||
conversations: nextConversations,
|
conversations: nextConversations,
|
||||||
|
isSending: hasInteractiveTool ? false : prev.isSending,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1585,6 +1660,113 @@ export function selectPendingAskUser(state = chatAutomat.state) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const selectHasPendingAsk = (state = chatAutomat.state) => Boolean(selectPendingAskUser(state));
|
export const selectHasPendingAsk = (state = chatAutomat.state) =>
|
||||||
|
Boolean(selectPendingAskUser(state) || selectPendingDraftRead(state));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns pending un-answered `read_file` tool call for a virtual draft (`drafts/*`).
|
||||||
|
* @param {object} [state]
|
||||||
|
* @returns {{ toolCallId: string, filename: string, draftKey: string } | null}
|
||||||
|
*/
|
||||||
|
export function selectPendingDraftRead(state = chatAutomat.state) {
|
||||||
|
const { messages, streamingText, activeTurns } = state;
|
||||||
|
|
||||||
|
// 1. Check live activeTurns for any read_file draft tool call
|
||||||
|
if (activeTurns && activeTurns.length > 0) {
|
||||||
|
for (let i = activeTurns.length - 1; i >= 0; i--) {
|
||||||
|
const turn = activeTurns[i];
|
||||||
|
const tools = turn.tools || [];
|
||||||
|
for (let j = tools.length - 1; j >= 0; j--) {
|
||||||
|
const tool = tools[j];
|
||||||
|
if (tool.name === 'read_file') {
|
||||||
|
let args = {};
|
||||||
|
try {
|
||||||
|
if (typeof tool.content === 'string') {
|
||||||
|
args = JSON.parse(tool.content);
|
||||||
|
} else if (tool.content && typeof tool.content === 'object') {
|
||||||
|
args = tool.content;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
if (!args.filename && !args.path && tool.arguments) {
|
||||||
|
try {
|
||||||
|
args = typeof tool.arguments === 'string' ? JSON.parse(tool.arguments) : tool.arguments;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
const draftInfo = getDraftInfoFromToolArgs(args);
|
||||||
|
if (draftInfo) {
|
||||||
|
return {
|
||||||
|
toolCallId: tool.id,
|
||||||
|
filename: draftInfo.filename,
|
||||||
|
draftKey: draftInfo.draftKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check message history for any un-answered read_file for drafts/*
|
||||||
|
if (streamingText || !messages || messages.length === 0) return null;
|
||||||
|
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg.role === 'system' || msg.role === 'usage' || msg.role === 'reasoning') continue;
|
||||||
|
if (msg.role === 'user') return null;
|
||||||
|
|
||||||
|
if (msg.role === 'tool') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.role === 'assistant' || msg.role === 'model') {
|
||||||
|
let payload = null;
|
||||||
|
if (typeof msg.message === 'string') {
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(msg.message);
|
||||||
|
} catch {}
|
||||||
|
} else if (typeof msg.message === 'object' && msg.message !== null) {
|
||||||
|
payload = msg.message;
|
||||||
|
}
|
||||||
|
if (!payload?.tool_calls || !Array.isArray(payload.tool_calls) || payload.tool_calls.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftCalls = payload.tool_calls.filter((tc) => tc.function?.name === 'read_file');
|
||||||
|
for (const tc of draftCalls) {
|
||||||
|
let args = {};
|
||||||
|
try {
|
||||||
|
if (typeof tc.function?.arguments === 'string') {
|
||||||
|
args = JSON.parse(tc.function.arguments);
|
||||||
|
} else if (tc.function?.arguments) {
|
||||||
|
args = tc.function.arguments;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
const draftInfo = getDraftInfoFromToolArgs(args);
|
||||||
|
if (draftInfo) {
|
||||||
|
const answered = messages.slice(i + 1).some((m) => {
|
||||||
|
if (m.role !== 'tool') return false;
|
||||||
|
try {
|
||||||
|
const p = typeof m.message === 'string' ? JSON.parse(m.message) : m.message;
|
||||||
|
return p?.tool_call_id === tc.id;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!answered) {
|
||||||
|
return {
|
||||||
|
toolCallId: tc.id,
|
||||||
|
filename: draftInfo.filename,
|
||||||
|
draftKey: draftInfo.draftKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectHasPendingDraftRead = (state = chatAutomat.state) => Boolean(selectPendingDraftRead(state));
|
||||||
|
|
||||||
export default chatAutomat;
|
export default chatAutomat;
|
||||||
|
|||||||
@@ -5,16 +5,11 @@
|
|||||||
* with backend VFS requests and broadcasts.
|
* with backend VFS requests and broadcasts.
|
||||||
*
|
*
|
||||||
* Incoming Signals:
|
* Incoming Signals:
|
||||||
* - SSE 'draft:request': Backend requests a draft (read or list).
|
|
||||||
* - SSE 'draft': Backend broadcasts an external update to a draft.
|
* - SSE 'draft': Backend broadcasts an external update to a draft.
|
||||||
*
|
|
||||||
* Outgoing Calls:
|
|
||||||
* - POST /api/vfs/drafts/response: Responds to backend requests with draft content or file list.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { deliveryEditorAutomat, createEmptyLineitem } from './deliveryEditorAutomat';
|
import { deliveryEditorAutomat, createEmptyLineitem } from './deliveryEditorAutomat';
|
||||||
import { quoteDraftAutomat, orderDraftAutomat } from './manifestDraftAutomat';
|
import { quoteDraftAutomat, orderDraftAutomat } from './manifestDraftAutomat';
|
||||||
import { vfs } from '../api/vfs';
|
|
||||||
import { getStoredClientId } from '../api/http';
|
import { getStoredClientId } from '../api/http';
|
||||||
import { getSseClientId } from './sseAutomat';
|
import { getSseClientId } from './sseAutomat';
|
||||||
|
|
||||||
@@ -140,33 +135,7 @@ export function applyDraftUpdate(rawName, rawContent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles incoming 'draft:request' SSE event from backend.
|
|
||||||
*
|
|
||||||
* @param {{ requestId: string, action: 'read'|'list', name?: string, target?: string }} payload
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export async function handleDraftRequest(payload) {
|
|
||||||
if (!payload || !payload.requestId) return;
|
|
||||||
const { requestId, action, name, target } = payload;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (action === 'read') {
|
|
||||||
const draftName = normalizeDraftName(name || target);
|
|
||||||
const content = getActiveDraftContent(draftName);
|
|
||||||
if (content != null) {
|
|
||||||
await vfs.respondDraft({ requestId, content });
|
|
||||||
} else {
|
|
||||||
await vfs.respondDraft({ requestId, error: 'Draft not open' });
|
|
||||||
}
|
|
||||||
} else if (action === 'list') {
|
|
||||||
const files = getActiveDraftsList();
|
|
||||||
await vfs.respondDraft({ requestId, files });
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[draft] error handling draft:request:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles incoming 'draft' SSE broadcast from backend.
|
* Handles incoming 'draft' SSE broadcast from backend.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import supplierAutomat from './supplierAutomat';
|
|||||||
import orderAutomat from './orderAutomat';
|
import orderAutomat from './orderAutomat';
|
||||||
import quoteAutomat from './quoteAutomat';
|
import quoteAutomat from './quoteAutomat';
|
||||||
import shipmentAutomat from './shipmentAutomat';
|
import shipmentAutomat from './shipmentAutomat';
|
||||||
import { handleDraftRequest, handleDraftEvent } from './draftSync';
|
import { handleDraftEvent } from './draftSync';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,14 +265,7 @@ function connect() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addTrackedListener(es, 'draft:request', (event) => {
|
|
||||||
try {
|
|
||||||
const payload = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
||||||
void handleDraftRequest(payload);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[sse] draft:request handling failed', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addTrackedListener(es, 'draft', (event) => {
|
addTrackedListener(es, 'draft', (event) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -37,12 +37,17 @@ import LightModeRoundedIcon from '@mui/icons-material/LightModeRounded';
|
|||||||
import CycloneRoundedIcon from '@mui/icons-material/CycloneRounded';
|
import CycloneRoundedIcon from '@mui/icons-material/CycloneRounded';
|
||||||
|
|
||||||
import { setting as settingApi } from '../../api';
|
import { setting as settingApi } from '../../api';
|
||||||
import chatAutomat, { selectPendingAskUser } from '../../automats/chatAutomat';
|
import chatAutomat, {
|
||||||
|
selectPendingAskUser,
|
||||||
|
selectPendingDraftRead,
|
||||||
|
} from '../../automats/chatAutomat';
|
||||||
|
import { getActiveDraftContent } from '../../automats/draftSync';
|
||||||
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
import userSettingsAutomat from '../../automats/userSettingsAutomat';
|
||||||
import vfsAutomat from '../../automats/vfsAutomat';
|
import vfsAutomat from '../../automats/vfsAutomat';
|
||||||
import { VfsEditorPanel, VfsFileIcon } from '../vfs';
|
import { VfsEditorPanel, VfsFileIcon } from '../vfs';
|
||||||
import ChatFlowList from './ChatFlowList';
|
import ChatFlowList from './ChatFlowList';
|
||||||
import AskUserPrompt from './AskUserPrompt';
|
import AskUserPrompt from './AskUserPrompt';
|
||||||
|
import DraftRequestPrompt from './DraftRequestPrompt';
|
||||||
import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
|
import { ACCEPT_STRING, readFileAttachment } from './chatAttachments';
|
||||||
import { ChatStagedAttachmentsTray } from './ChatAttachmentItems';
|
import { ChatStagedAttachmentsTray } from './ChatAttachmentItems';
|
||||||
import {
|
import {
|
||||||
@@ -836,6 +841,33 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getPendingAskUser = () => selectPendingAskUser(this.state);
|
getPendingAskUser = () => selectPendingAskUser(this.state);
|
||||||
|
getPendingDraftRead = () => selectPendingDraftRead(this.state);
|
||||||
|
|
||||||
|
handleProvideDraft = async ({ toolCallId, draftKey, empty = false }) => {
|
||||||
|
try {
|
||||||
|
let content;
|
||||||
|
if (empty) {
|
||||||
|
content = { error: 'Draft not open or empty' };
|
||||||
|
} else {
|
||||||
|
const manifest = getActiveDraftContent(draftKey);
|
||||||
|
content = {
|
||||||
|
manifest: manifest || {
|
||||||
|
kind: draftKey,
|
||||||
|
mode: 'create',
|
||||||
|
lines: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await chatAutomat.actions.sendToolResponse({
|
||||||
|
tool_call_id: toolCallId,
|
||||||
|
name: 'read_file',
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ChatDrawer] Failed to provide draft response:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
renderHeader() {
|
renderHeader() {
|
||||||
const {
|
const {
|
||||||
@@ -1332,6 +1364,7 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pendingAsk = this.getPendingAskUser();
|
const pendingAsk = this.getPendingAskUser();
|
||||||
|
const pendingDraftRead = this.getPendingDraftRead();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
|
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
|
||||||
@@ -1366,6 +1399,14 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
disabled={this.state.isSending}
|
disabled={this.state.isSending}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pendingDraftRead && (
|
||||||
|
<DraftRequestPrompt
|
||||||
|
draftData={pendingDraftRead}
|
||||||
|
onProvideDraft={this.handleProvideDraft}
|
||||||
|
disabled={this.state.isSending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1388,9 +1429,13 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const pendingAsk = this.getPendingAskUser();
|
const pendingAsk = this.getPendingAskUser();
|
||||||
|
const pendingDraftRead = this.getPendingDraftRead();
|
||||||
const isBusy = conversationState === 'busy';
|
const isBusy = conversationState === 'busy';
|
||||||
const isWorking = isBusy || isSending;
|
const isWorking = isBusy || isSending;
|
||||||
const canChat = (Boolean(selectedConversationId) || draftConversation) && !pendingAsk;
|
const canChat =
|
||||||
|
(Boolean(selectedConversationId) || draftConversation) &&
|
||||||
|
!pendingAsk &&
|
||||||
|
!pendingDraftRead;
|
||||||
const canSend =
|
const canSend =
|
||||||
!isWorking &&
|
!isWorking &&
|
||||||
canChat &&
|
canChat &&
|
||||||
@@ -1762,6 +1807,8 @@ export default class ChatDrawer extends PureComponent {
|
|||||||
placeholder={
|
placeholder={
|
||||||
pendingAsk
|
pendingAsk
|
||||||
? 'Bitte Frage oben beantworten…'
|
? 'Bitte Frage oben beantworten…'
|
||||||
|
: pendingDraftRead
|
||||||
|
? 'Bitte Entwurfsanfrage oben beantworten…'
|
||||||
: stagedAttachments.length > 0
|
: stagedAttachments.length > 0
|
||||||
? 'Nachricht hinzufügen oder direkt absenden…'
|
? 'Nachricht hinzufügen oder direkt absenden…'
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
118
src/components/chat/DraftRequestPrompt.jsx
Normal file
118
src/components/chat/DraftRequestPrompt.jsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
|
||||||
|
import SendRoundedIcon from '@mui/icons-material/SendRounded';
|
||||||
|
import BlockRoundedIcon from '@mui/icons-material/BlockRounded';
|
||||||
|
import { getActiveDraftContent } from '../../automats/draftSync';
|
||||||
|
import {
|
||||||
|
ASK_PROMPT_BG_PAIR,
|
||||||
|
ASK_PROMPT_BORDER_PAIR,
|
||||||
|
byMode,
|
||||||
|
} from '../../colors';
|
||||||
|
|
||||||
|
export default function DraftRequestPrompt({ draftData, onProvideDraft, disabled = false }) {
|
||||||
|
const { toolCallId, filename, draftKey } = draftData;
|
||||||
|
const manifest = getActiveDraftContent(draftKey);
|
||||||
|
const rawLines = manifest?.lines ?? manifest?.lineitems ?? [];
|
||||||
|
const linesCount = Array.isArray(rawLines) ? rawLines.length : 0;
|
||||||
|
const hasContent = Boolean(manifest && (linesCount > 0 || manifest.partyId || manifest.supplierId));
|
||||||
|
|
||||||
|
const handleSendDraft = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
onProvideDraft({ toolCallId, draftKey, empty: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendEmpty = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
onProvideDraft({ toolCallId, draftKey, empty: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const draftLabel =
|
||||||
|
draftKey === 'delivery'
|
||||||
|
? 'Lieferung'
|
||||||
|
: draftKey === 'quote'
|
||||||
|
? 'Angebot'
|
||||||
|
: draftKey === 'order'
|
||||||
|
? 'Bestellung'
|
||||||
|
: draftKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
mx: 2,
|
||||||
|
my: 1,
|
||||||
|
p: 2,
|
||||||
|
borderRadius: 3,
|
||||||
|
bgcolor: byMode(theme, ASK_PROMPT_BG_PAIR),
|
||||||
|
border: `1px solid ${byMode(theme, ASK_PROMPT_BORDER_PAIR)}`,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1.5,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, color: 'primary.main' }}>
|
||||||
|
<DescriptionOutlinedIcon sx={{ fontSize: 20 }} />
|
||||||
|
<Typography variant="subtitle2" fontWeight={700} sx={{ color: 'text.primary' }}>
|
||||||
|
Entwurfsanfrage: {filename || `drafts/${draftKey}`}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontSize: '0.82rem', lineHeight: 1.4 }}>
|
||||||
|
{hasContent ? (
|
||||||
|
<>
|
||||||
|
Das Modell möchte den aktuellen <strong>{draftLabel}-Entwurf</strong> ({linesCount} Position{linesCount === 1 ? '' : 'en'}) einlesen, um die Bearbeitung fortzusetzen.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Das Modell fordert <strong>{filename}</strong> an. Aktuell ist kein {draftLabel}-Entwurf geöffnet.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 0.5 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleSendDraft}
|
||||||
|
startIcon={<SendRoundedIcon sx={{ fontSize: 16 }} />}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 2,
|
||||||
|
py: 0.75,
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasContent ? `Aktuellen ${draftLabel}-Entwurf senden` : 'Leeren Entwurf senden'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleSendEmpty}
|
||||||
|
startIcon={<BlockRoundedIcon sx={{ fontSize: 16 }} />}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 1.5,
|
||||||
|
py: 0.75,
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
color: 'text.secondary',
|
||||||
|
borderColor: 'divider',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'error.main',
|
||||||
|
color: 'error.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nicht verfügbar melden
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export { default as ChatFab } from './ChatFab';
|
|||||||
export { default as ChatDrawer } from './ChatDrawer';
|
export { default as ChatDrawer } from './ChatDrawer';
|
||||||
export { default as ChatFlowList } from './ChatFlowList';
|
export { default as ChatFlowList } from './ChatFlowList';
|
||||||
export { default as NewConversationDialog } from './NewConversationDialog';
|
export { default as NewConversationDialog } from './NewConversationDialog';
|
||||||
|
export { default as DraftRequestPrompt } from './DraftRequestPrompt';
|
||||||
|
|||||||
Reference in New Issue
Block a user