wip: first run of implementation
This commit is contained in:
@@ -5,9 +5,10 @@ import './AssistantPanelControls.css';
|
||||
interface AssistantPanelControlsProps {
|
||||
elements: AssistantPanelElement[];
|
||||
onAction: (action: string, payload?: Record<string, unknown>) => void;
|
||||
actionPolicies?: Record<string, 'silent' | 'confirm' | 'danger'>;
|
||||
}
|
||||
|
||||
export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({ elements, onAction }) => {
|
||||
export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({ elements, onAction, actionPolicies = {} }) => {
|
||||
const [widgetValues, setWidgetValues] = useState<Record<string, unknown>>({});
|
||||
const [activeTabByWidget, setActiveTabByWidget] = useState<Record<string, string>>({});
|
||||
|
||||
@@ -21,6 +22,20 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
const getWidgetValue = (key: string, defaultValue?: unknown) =>
|
||||
Object.prototype.hasOwnProperty.call(widgetValues, key) ? widgetValues[key] : defaultValue;
|
||||
|
||||
const triggerAction = (action: string, payload?: Record<string, unknown>, label?: string) => {
|
||||
const policy = actionPolicies[action] || 'silent';
|
||||
|
||||
if (policy !== 'silent') {
|
||||
const confirmationText = label || action;
|
||||
const confirmed = window.confirm(confirmationText);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onAction(action, payload);
|
||||
};
|
||||
|
||||
const renderInputControl = (
|
||||
key: string,
|
||||
label: string,
|
||||
@@ -150,7 +165,7 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
{element.action && element.submitLabel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAction(element.action!, { ...(element.payload ?? {}), [element.key]: currentValue })}
|
||||
onClick={() => triggerAction(element.action!, { ...(element.payload ?? {}), [element.key]: currentValue }, element.submitLabel)}
|
||||
>
|
||||
{element.submitLabel}
|
||||
</button>
|
||||
@@ -175,7 +190,7 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
{element.action && element.submitLabel && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAction(element.action!, { ...(element.payload ?? {}), [element.key]: currentValue })}
|
||||
onClick={() => triggerAction(element.action!, { ...(element.payload ?? {}), [element.key]: currentValue }, element.submitLabel)}
|
||||
>
|
||||
{element.submitLabel}
|
||||
</button>
|
||||
@@ -191,11 +206,11 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
onAction(element.action, {
|
||||
triggerAction(element.action, {
|
||||
...(element.payload ?? {}),
|
||||
formId: element.formId,
|
||||
values,
|
||||
});
|
||||
}, element.submitLabel);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -224,7 +239,7 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
<button
|
||||
key={`assistant-card-action-${indexPath}-${actionIndex}`}
|
||||
type="button"
|
||||
onClick={() => onAction(action.action, action.payload)}
|
||||
onClick={() => triggerAction(action.action, action.payload, action.label)}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
@@ -243,7 +258,7 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
alt={element.alt || ''}
|
||||
onClick={() => {
|
||||
if (element.action) {
|
||||
onAction(element.action, element.payload);
|
||||
triggerAction(element.action, element.payload, element.caption || element.alt || element.action);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -279,7 +294,7 @@ export const AssistantPanelControls: React.FC<AssistantPanelControlsProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<button key={`assistant-element-${indexPath}`} type="button" onClick={() => onAction(element.action, element.payload)}>
|
||||
<button key={`assistant-element-${indexPath}`} type="button" onClick={() => triggerAction(element.action, element.payload, element.label)}>
|
||||
{element.label}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { resolveAssistantEditorContext } from '../../navigation/assistantPromptC
|
||||
import { planAssistantRequest } from '../../navigation/assistantConversation';
|
||||
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
|
||||
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
|
||||
import { toClarificationElements } from '../../navigation/protocolNeedsInput';
|
||||
import { ensureConversationId } from '../../navigation/chatSession';
|
||||
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
|
||||
import { useChatMessageSender } from '../../navigation/useChatMessageSender';
|
||||
@@ -22,6 +23,7 @@ export const AssistantSidebar: React.FC = () => {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||
const [panelElements, setPanelElements] = useState<AssistantPanelElement[]>([]);
|
||||
const [actionPolicies, setActionPolicies] = useState<Record<string, 'silent' | 'confirm' | 'danger'>>({});
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
@@ -127,10 +129,23 @@ export const AssistantSidebar: React.FC = () => {
|
||||
throw new Error(sendResult.error || 'Failed to send assistant message');
|
||||
}
|
||||
|
||||
if (sendResult.message) {
|
||||
if (sendResult.envelope) {
|
||||
finalizeAssistantTurn(resolvedConversationId, sendResult.envelope.assistantText);
|
||||
const uiElements = Array.isArray(sendResult.envelope.ui?.elements)
|
||||
? (sendResult.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(sendResult.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
sendResult.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
} else if (sendResult.message) {
|
||||
const parsedResponse = extractAssistantResponseContent(sendResult.message);
|
||||
finalizeAssistantTurn(resolvedConversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
} else {
|
||||
appendAssistantMessage(resolvedConversationId, tr('chat.errorEmptyResponse'));
|
||||
stopStreaming();
|
||||
@@ -146,6 +161,61 @@ export const AssistantSidebar: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleAssistantAction = (action: string, payload?: Record<string, unknown>) => {
|
||||
if (action === 'submitNeedsInput' && conversationId) {
|
||||
const values = payload?.values;
|
||||
if (!values || typeof values !== 'object') {
|
||||
setActionError(tr('assistantSidebar.error.actionFailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const clarificationMessage = `needs_input_response: ${JSON.stringify(values)}`;
|
||||
|
||||
beginUserTurn(conversationId, clarificationMessage);
|
||||
|
||||
void sendChatMessage({
|
||||
conversationId,
|
||||
message: clarificationMessage,
|
||||
metadata: { surface: 'sidebar' },
|
||||
}).then((sendResult) => {
|
||||
if (!sendResult.success) {
|
||||
appendAssistantMessage(
|
||||
conversationId,
|
||||
tr('chat.errorPrefix', { error: sendResult.error || tr('chat.errorNoResponse') }),
|
||||
);
|
||||
stopStreaming();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendResult.envelope) {
|
||||
finalizeAssistantTurn(conversationId, sendResult.envelope.assistantText);
|
||||
const uiElements = Array.isArray(sendResult.envelope.ui?.elements)
|
||||
? (sendResult.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(sendResult.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
sendResult.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendResult.message) {
|
||||
const parsedResponse = extractAssistantResponseContent(sendResult.message);
|
||||
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to submit assistant clarification:', error);
|
||||
appendAssistantMessage(conversationId, tr('chat.errorGeneric'));
|
||||
stopStreaming();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const result = dispatchAssistantAction(
|
||||
{
|
||||
action,
|
||||
@@ -230,7 +300,7 @@ export const AssistantSidebar: React.FC = () => {
|
||||
)}
|
||||
|
||||
{panelElements.length > 0 && (
|
||||
<AssistantPanelControls elements={panelElements} onAction={handleAssistantAction} />
|
||||
<AssistantPanelControls elements={panelElements} onAction={handleAssistantAction} actionPolicies={actionPolicies} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useChatSurfaceState } from '../../navigation/useChatSurfaceState';
|
||||
import { getChatSurfaceMode } from '../../navigation/chatSurfaceMode';
|
||||
import { dispatchAssistantAction } from '../../navigation/assistantActionDispatcher';
|
||||
import { extractAssistantResponseContent, type AssistantPanelElement } from '../../navigation/assistantPanelSpec';
|
||||
import { toClarificationElements } from '../../navigation/protocolNeedsInput';
|
||||
import { useAppStore } from '../../store';
|
||||
import { ChatTranscript } from '../ChatSurface';
|
||||
import { AssistantPanelControls } from '../AssistantPanelControls';
|
||||
@@ -28,6 +29,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
const [apiKeyError, setApiKeyError] = useState('');
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [panelElements, setPanelElements] = useState<AssistantPanelElement[]>([]);
|
||||
const [actionPolicies, setActionPolicies] = useState<Record<string, 'silent' | 'confirm' | 'danger'>>({});
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -190,21 +192,36 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
// Fall back to the backend result message if streaming didn't capture the content
|
||||
const assistantContent = getStreamingContent() || (result.success ? result.message : '');
|
||||
|
||||
if (assistantContent) {
|
||||
if (result.envelope) {
|
||||
finalizeAssistantTurn(conversationId, result.envelope.assistantText);
|
||||
const uiElements = Array.isArray(result.envelope.ui?.elements)
|
||||
? (result.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(result.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
result.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
} else if (assistantContent) {
|
||||
const parsedResponse = extractAssistantResponseContent(assistantContent);
|
||||
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
} else if (!result.success) {
|
||||
// Backend returned an error (API failure, model unavailable, etc.)
|
||||
appendAssistantMessage(conversationId, tr('chat.errorPrefix', { error: result.error || tr('chat.errorNoResponse') }));
|
||||
stopStreaming();
|
||||
setPanelElements([]);
|
||||
setActionPolicies({});
|
||||
} else {
|
||||
// No content from streaming AND no error, but also no success message
|
||||
// This can happen with some models that don't return content properly
|
||||
appendAssistantMessage(conversationId, tr('chat.errorEmptyResponse'));
|
||||
stopStreaming();
|
||||
setPanelElements([]);
|
||||
setActionPolicies({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to send message:', error);
|
||||
@@ -226,7 +243,64 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleNeedsInputSubmit = async (payload?: Record<string, unknown>) => {
|
||||
const values = payload?.values;
|
||||
if (!values || typeof values !== 'object') {
|
||||
setActionError(tr('assistantSidebar.error.actionFailed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const clarificationMessage = `needs_input_response: ${JSON.stringify(values)}`;
|
||||
beginUserTurn(conversationId, clarificationMessage);
|
||||
|
||||
try {
|
||||
const result = await sendChatMessage({
|
||||
conversationId,
|
||||
message: clarificationMessage,
|
||||
metadata: { surface: 'tab' },
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
appendAssistantMessage(conversationId, tr('chat.errorPrefix', { error: result.error || tr('chat.errorNoResponse') }));
|
||||
stopStreaming();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.envelope) {
|
||||
finalizeAssistantTurn(conversationId, result.envelope.assistantText);
|
||||
const uiElements = Array.isArray(result.envelope.ui?.elements)
|
||||
? (result.envelope.ui?.elements as AssistantPanelElement[])
|
||||
: toClarificationElements(result.envelope.needsInput);
|
||||
setPanelElements(uiElements);
|
||||
setActionPolicies(
|
||||
result.envelope.actions.reduce<Record<string, 'silent' | 'confirm' | 'danger'>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const assistantContent = getStreamingContent() || result.message;
|
||||
if (assistantContent) {
|
||||
const parsedResponse = extractAssistantResponseContent(assistantContent);
|
||||
finalizeAssistantTurn(conversationId, parsedResponse.displayText);
|
||||
setPanelElements(parsedResponse.panelSpec?.elements ?? []);
|
||||
setActionPolicies({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to submit clarification:', error);
|
||||
appendAssistantMessage(conversationId, tr('chat.errorGeneric'));
|
||||
stopStreaming();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssistantAction = (action: string, payload?: Record<string, unknown>) => {
|
||||
if (action === 'submitNeedsInput') {
|
||||
void handleNeedsInputSubmit(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = dispatchAssistantAction(
|
||||
{
|
||||
action,
|
||||
@@ -377,7 +451,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
||||
/>
|
||||
|
||||
{panelElements.length > 0 && (
|
||||
<AssistantPanelControls elements={panelElements} onAction={handleAssistantAction} />
|
||||
<AssistantPanelControls elements={panelElements} onAction={handleAssistantAction} actionPolicies={actionPolicies} />
|
||||
)}
|
||||
|
||||
{actionError && <p className="chat-surface-error">{actionError}</p>}
|
||||
|
||||
@@ -1478,6 +1478,12 @@ interface CategoryCount {
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface DashboardProtocolHealth {
|
||||
blockedActionCount: number;
|
||||
parseValidityRate: number;
|
||||
fallbackTurns: number;
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const { t: tr, language } = useI18n();
|
||||
const { posts, media } = useAppStore();
|
||||
@@ -1486,6 +1492,7 @@ const Dashboard: React.FC = () => {
|
||||
const [tagCounts, setTagCounts] = useState<TagCount[]>([]);
|
||||
const [tagColors, setTagColors] = useState<Map<string, string>>(new Map());
|
||||
const [categoryCounts, setCategoryCounts] = useState<CategoryCount[]>([]);
|
||||
const [protocolHealth, setProtocolHealth] = useState<DashboardProtocolHealth | null>(null);
|
||||
|
||||
const uiDateLocale = UI_DATE_LOCALE[language] || UI_DATE_LOCALE.en;
|
||||
const monthFormatter = useMemo(
|
||||
@@ -1496,17 +1503,25 @@ const Dashboard: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const [ds, ym, tc, cc, colorMap] = await Promise.all([
|
||||
const [ds, ym, tc, cc, colorMap, protocolHealthSnapshot] = await Promise.all([
|
||||
window.electronAPI?.posts.getDashboardStats(),
|
||||
window.electronAPI?.posts.getByYearMonth(),
|
||||
window.electronAPI?.posts.getTagsWithCounts(),
|
||||
window.electronAPI?.posts.getCategoriesWithCounts(),
|
||||
loadTagColorMap(),
|
||||
window.electronAPI?.chat.getProtocolHealth(),
|
||||
]);
|
||||
if (ds) setStats(ds);
|
||||
if (ym) setYearMonthData(ym);
|
||||
if (tc) setTagCounts(tc);
|
||||
if (cc) setCategoryCounts(cc);
|
||||
if (protocolHealthSnapshot) {
|
||||
setProtocolHealth({
|
||||
blockedActionCount: protocolHealthSnapshot.blockedActionCount,
|
||||
parseValidityRate: protocolHealthSnapshot.parseValidityRate,
|
||||
fallbackTurns: protocolHealthSnapshot.fallbackTurns,
|
||||
});
|
||||
}
|
||||
setTagColors(colorMap);
|
||||
} catch (e) {
|
||||
console.error('Failed to load dashboard stats:', e);
|
||||
@@ -1551,6 +1566,9 @@ const Dashboard: React.FC = () => {
|
||||
const displayDraftCount = stats?.draftCount ?? 0;
|
||||
const displayPublishedCount = stats?.publishedCount ?? 0;
|
||||
const displayArchivedCount = stats?.archivedCount ?? 0;
|
||||
const parseValidityPercent = protocolHealth
|
||||
? `${Math.round(protocolHealth.parseValidityRate * 100)}%`
|
||||
: '—';
|
||||
|
||||
const getPostCountLabel = useCallback((count: number) => {
|
||||
return tr(count === 1 ? 'dashboard.postCount.one' : 'dashboard.postCount.other', { count });
|
||||
@@ -1597,6 +1615,14 @@ const Dashboard: React.FC = () => {
|
||||
<span className="stat-tag">{tr('dashboard.stats.categories', { count: categoryCounts.length })}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-number">{parseValidityPercent}</div>
|
||||
<div className="stat-label">{tr('dashboard.stats.protocolHealth')}</div>
|
||||
<div className="stat-breakdown">
|
||||
<span className="stat-tag">{tr('dashboard.stats.blockedActions', { count: protocolHealth?.blockedActionCount ?? 0 })}</span>
|
||||
<span className="stat-tag">{tr('dashboard.stats.fallbackTurns', { count: protocolHealth?.fallbackTurns ?? 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{timelineEntries.length > 0 && (
|
||||
|
||||
@@ -528,6 +528,9 @@
|
||||
"dashboard.stats.images": "{count} Bilder",
|
||||
"dashboard.stats.tags": "Schlagwörter",
|
||||
"dashboard.stats.categories": "{count} Kategorien",
|
||||
"dashboard.stats.protocolHealth": "Protokollzustand",
|
||||
"dashboard.stats.blockedActions": "{count} blockierte Aktionen",
|
||||
"dashboard.stats.fallbackTurns": "{count} Fallback-Durchläufe",
|
||||
"dashboard.section.postsOverTime": "Beiträge im Zeitverlauf",
|
||||
"dashboard.section.tags": "Schlagwörter",
|
||||
"dashboard.section.categories": "Kategorien",
|
||||
|
||||
@@ -528,6 +528,9 @@
|
||||
"dashboard.stats.images": "{count} images",
|
||||
"dashboard.stats.tags": "Tags",
|
||||
"dashboard.stats.categories": "{count} categories",
|
||||
"dashboard.stats.protocolHealth": "Protocol Health",
|
||||
"dashboard.stats.blockedActions": "{count} blocked actions",
|
||||
"dashboard.stats.fallbackTurns": "{count} fallback turns",
|
||||
"dashboard.section.postsOverTime": "Posts Over Time",
|
||||
"dashboard.section.tags": "Tags",
|
||||
"dashboard.section.categories": "Categories",
|
||||
|
||||
@@ -528,6 +528,9 @@
|
||||
"dashboard.stats.images": "{count} imágenes",
|
||||
"dashboard.stats.tags": "Etiquetas",
|
||||
"dashboard.stats.categories": "{count} categorías",
|
||||
"dashboard.stats.protocolHealth": "Salud del protocolo",
|
||||
"dashboard.stats.blockedActions": "{count} acciones bloqueadas",
|
||||
"dashboard.stats.fallbackTurns": "{count} respuestas de respaldo",
|
||||
"dashboard.section.postsOverTime": "Entradas a lo largo del tiempo",
|
||||
"dashboard.section.tags": "Etiquetas",
|
||||
"dashboard.section.categories": "Categorías",
|
||||
|
||||
@@ -528,6 +528,9 @@
|
||||
"dashboard.stats.images": "{count} images",
|
||||
"dashboard.stats.tags": "Étiquettes",
|
||||
"dashboard.stats.categories": "{count} catégories",
|
||||
"dashboard.stats.protocolHealth": "Santé du protocole",
|
||||
"dashboard.stats.blockedActions": "{count} actions bloquées",
|
||||
"dashboard.stats.fallbackTurns": "{count} tours de secours",
|
||||
"dashboard.section.postsOverTime": "Articles dans le temps",
|
||||
"dashboard.section.tags": "Étiquettes",
|
||||
"dashboard.section.categories": "Catégories",
|
||||
|
||||
@@ -528,6 +528,9 @@
|
||||
"dashboard.stats.images": "{count} immagini",
|
||||
"dashboard.stats.tags": "Tag",
|
||||
"dashboard.stats.categories": "{count} categorie",
|
||||
"dashboard.stats.protocolHealth": "Salute del protocollo",
|
||||
"dashboard.stats.blockedActions": "{count} azioni bloccate",
|
||||
"dashboard.stats.fallbackTurns": "{count} risposte di fallback",
|
||||
"dashboard.section.postsOverTime": "Post nel tempo",
|
||||
"dashboard.section.tags": "Tag",
|
||||
"dashboard.section.categories": "Categorie",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ProtocolResponseEnvelope } from '../types/electron';
|
||||
|
||||
export interface ChatService {
|
||||
createConversation: (title?: string, model?: string) => Promise<{ id: string } | null | undefined>;
|
||||
sendMessage: (
|
||||
conversationId: string,
|
||||
message: string,
|
||||
metadata?: SendMessageMetadata,
|
||||
) => Promise<{ success: boolean; message?: string; error?: string } | null | undefined>;
|
||||
) => Promise<{ success: boolean; message?: string; envelope?: ProtocolResponseEnvelope; protocolVersion?: '2.0'; traceId?: string; warnings?: string[]; error?: string } | null | undefined>;
|
||||
}
|
||||
|
||||
export interface SendMessageMetadata {
|
||||
@@ -27,6 +29,10 @@ export interface SendConversationMessageInput {
|
||||
export interface SendConversationMessageResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
envelope?: ProtocolResponseEnvelope;
|
||||
protocolVersion?: '2.0';
|
||||
traceId?: string;
|
||||
warnings?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -69,5 +75,9 @@ export async function sendConversationMessage(
|
||||
return {
|
||||
success: true,
|
||||
message: result.message || '',
|
||||
envelope: result.envelope,
|
||||
protocolVersion: result.protocolVersion,
|
||||
traceId: result.traceId,
|
||||
warnings: result.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
38
src/renderer/navigation/protocolNeedsInput.ts
Normal file
38
src/renderer/navigation/protocolNeedsInput.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { ProtocolNeedsInputField, ProtocolResponseEnvelope } from '../types/electron';
|
||||
import type { AssistantPanelElement } from './assistantPanelSpec';
|
||||
|
||||
function toFormField(field: ProtocolNeedsInputField): {
|
||||
key: string;
|
||||
label: string;
|
||||
inputType: 'text' | 'textarea' | 'select' | 'checkbox' | 'date' | 'number';
|
||||
placeholder?: string;
|
||||
defaultValue?: string | number | boolean;
|
||||
options?: Array<{ label: string; value: string }>;
|
||||
required?: boolean;
|
||||
} {
|
||||
return {
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
inputType: field.inputType,
|
||||
placeholder: field.placeholder,
|
||||
defaultValue: field.defaultValue,
|
||||
options: field.options,
|
||||
required: field.required,
|
||||
};
|
||||
}
|
||||
|
||||
export function toClarificationElements(
|
||||
needsInput: ProtocolResponseEnvelope['needsInput'],
|
||||
): AssistantPanelElement[] {
|
||||
if (!needsInput.required || needsInput.fields.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
type: 'form',
|
||||
formId: 'agui-needs-input',
|
||||
submitLabel: needsInput.fields[0].label,
|
||||
action: 'submitNeedsInput',
|
||||
fields: needsInput.fields.map(toFormField),
|
||||
}];
|
||||
}
|
||||
@@ -176,6 +176,7 @@ const METHODS_V1: PythonApiMethodContractV1[] = [
|
||||
method('chat.validateApiKey', 'Validate chat API key and list available models.', [requiredString('apiKey')], '{ isValid: boolean; models: ChatModel[] }'),
|
||||
method('chat.setApiKey', 'Store chat API key.', [requiredString('apiKey')], '{ success: boolean; error?: string }'),
|
||||
method('chat.getApiKey', 'Get stored chat API key status.', [], 'ChatApiKeyStatus'),
|
||||
method('chat.getProtocolHealth', 'Get AGUI protocol telemetry health snapshot.', [], 'ProtocolTelemetrySnapshot'),
|
||||
method('chat.getAvailableModels', 'Get available chat models and selected default.', [], '{ success: boolean; models?: ChatModel[]; selectedModel?: string; error?: string }'),
|
||||
method('chat.setDefaultModel', 'Set default chat model.', [requiredString('modelId')], '{ success: boolean; error?: string }'),
|
||||
method('chat.getSystemPrompt', 'Get configured system prompt.', [], '{ success: boolean; prompt?: string; error?: string }'),
|
||||
@@ -359,11 +360,25 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
|
||||
{ name: 'maskedKey', type: 'string', required: true, description: 'Masked key representation for UI display.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ProtocolTelemetrySnapshot',
|
||||
description: 'Aggregated protocol telemetry metrics for AGUI response health.',
|
||||
fields: [
|
||||
{ name: 'totalTurns', type: 'number', required: true, description: 'Total number of recorded assistant turns.' },
|
||||
{ name: 'validEnvelopeTurns', type: 'number', required: true, description: 'Turns with schema-valid protocol envelopes.' },
|
||||
{ name: 'repairAttempts', type: 'number', required: true, description: 'Number of response repair attempts.' },
|
||||
{ name: 'fallbackTurns', type: 'number', required: true, description: 'Turns that used protocol fallback response.' },
|
||||
{ name: 'blockedActionCount', type: 'number', required: true, description: 'Count of actions blocked by policy.' },
|
||||
{ name: 'parseValidityRate', type: 'number', required: true, description: 'Ratio of valid envelopes to total turns.' },
|
||||
{ name: 'repairRate', type: 'number', required: true, description: 'Ratio of repair attempts to total turns.' },
|
||||
{ name: 'fallbackRate', type: 'number', required: true, description: 'Ratio of fallback turns to total turns.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const BDS_PYTHON_API_CONTRACT_V1: PythonApiContractV1 = {
|
||||
version: '1.3.0',
|
||||
generatedAt: '2026-02-24T00:00:00.000Z',
|
||||
version: '1.4.0',
|
||||
generatedAt: '2026-02-25T00:00:00.000Z',
|
||||
methods: METHODS_V1,
|
||||
dataStructures: DATA_STRUCTURES_V1,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user