wip: complete rework first round
This commit is contained in:
@@ -156,276 +156,3 @@ export const assistantPanelSpecSchema = z.object({
|
||||
|
||||
export type AssistantPanelElement = z.infer<typeof assistantPanelElementSchema>;
|
||||
export type AssistantPanelSpec = z.infer<typeof assistantPanelSpecSchema>;
|
||||
|
||||
export interface AssistantResponseContent {
|
||||
displayText: string;
|
||||
panelSpec: AssistantPanelSpec | null;
|
||||
}
|
||||
|
||||
function toRecord(value: unknown): Record<string, unknown> | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function normalizeChartElement(record: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const chartType = record.chartType;
|
||||
const normalized: Record<string, unknown> = {
|
||||
type: 'chart',
|
||||
chartType: chartType === 'line' || chartType === 'pie' ? chartType : 'bar',
|
||||
};
|
||||
|
||||
if (typeof record.title === 'string' && record.title.trim().length > 0) {
|
||||
normalized.title = record.title;
|
||||
}
|
||||
|
||||
if (Array.isArray(record.series)) {
|
||||
const series = record.series
|
||||
.map((entry) => {
|
||||
const item = toRecord(entry);
|
||||
if (!item || typeof item.label !== 'string' || typeof item.value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return { label: item.label, value: item.value };
|
||||
})
|
||||
.filter((entry): entry is { label: string; value: number } => Boolean(entry));
|
||||
|
||||
if (series.length > 0) {
|
||||
normalized.series = series;
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
const dataRecord = toRecord(record.data);
|
||||
|
||||
if (!dataRecord) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const labels = Array.isArray(dataRecord.labels) ? dataRecord.labels : [];
|
||||
const datasets = Array.isArray(dataRecord.datasets) ? dataRecord.datasets : [];
|
||||
const firstDataset = toRecord(datasets[0]);
|
||||
const values = Array.isArray(firstDataset?.data) ? firstDataset?.data : [];
|
||||
|
||||
if (labels.length === 0 || values.length === 0) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const series = labels
|
||||
.map((label, index) => ({
|
||||
label: String(label),
|
||||
value: Number(values[index]),
|
||||
}))
|
||||
.filter((entry) => Number.isFinite(entry.value));
|
||||
|
||||
if (series.length === 0) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
normalized.series = series;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeTabContent(tabValue: unknown): Record<string, unknown>[] {
|
||||
if (Array.isArray(tabValue)) {
|
||||
return tabValue.map((entry) => normalizeElement(entry)).filter((entry): entry is Record<string, unknown> => Boolean(entry));
|
||||
}
|
||||
|
||||
const normalized = normalizeElement(tabValue);
|
||||
return normalized ? [normalized] : [];
|
||||
}
|
||||
|
||||
function normalizeTabsElement(record: Record<string, unknown>): Record<string, unknown> | null {
|
||||
const tabs = Array.isArray(record.tabs) ? record.tabs : [];
|
||||
const normalizedTabs = tabs
|
||||
.map((tabValue, tabIndex) => {
|
||||
const tabRecord = toRecord(tabValue);
|
||||
if (!tabRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = typeof tabRecord.id === 'string' && tabRecord.id.trim().length > 0
|
||||
? tabRecord.id
|
||||
: `tab-${tabIndex + 1}`;
|
||||
|
||||
const label = typeof tabRecord.label === 'string' && tabRecord.label.trim().length > 0
|
||||
? tabRecord.label
|
||||
: typeof tabRecord.title === 'string' && tabRecord.title.trim().length > 0
|
||||
? tabRecord.title
|
||||
: id;
|
||||
|
||||
const elements = Array.isArray(tabRecord.elements)
|
||||
? normalizeTabContent(tabRecord.elements)
|
||||
: normalizeTabContent(tabRecord.content);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
elements,
|
||||
};
|
||||
})
|
||||
.filter((entry): entry is { id: string; label: string; elements: Record<string, unknown>[] } => Boolean(entry));
|
||||
|
||||
if (normalizedTabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
tabs: normalizedTabs,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeElement(value: unknown): Record<string, unknown> | null {
|
||||
const record = toRecord(value);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = typeof record.type === 'string' ? record.type : '';
|
||||
if (type === 'text' && typeof record.content === 'string' && typeof record.text !== 'string') {
|
||||
return { type: 'text', text: record.content };
|
||||
}
|
||||
|
||||
if (type === 'markdown') {
|
||||
const textValue = typeof record.content === 'string' ? record.content : typeof record.text === 'string' ? record.text : '';
|
||||
if (!textValue.trim()) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'text',
|
||||
text: textValue,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'chart') {
|
||||
return normalizeChartElement(record);
|
||||
}
|
||||
|
||||
if (type === 'tabs') {
|
||||
return normalizeTabsElement(record);
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
function normalizeCandidate(parsed: unknown): AssistantPanelSpec | null {
|
||||
const canonicalResult = assistantPanelSpecSchema.safeParse(parsed);
|
||||
if (canonicalResult.success) {
|
||||
return canonicalResult.data;
|
||||
}
|
||||
|
||||
const record = toRecord(parsed);
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (record.protocolVersion === '2.0' && record.ui) {
|
||||
return normalizeCandidate(record.ui);
|
||||
}
|
||||
|
||||
if (record.type === 'tab' && record.content) {
|
||||
return normalizeCandidate(record.content);
|
||||
}
|
||||
|
||||
if (record.type === 'tabs') {
|
||||
const tabsElement = normalizeTabsElement(record);
|
||||
if (!tabsElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asSpec = {
|
||||
specVersion: '1',
|
||||
elements: [tabsElement],
|
||||
};
|
||||
const normalizedResult = assistantPanelSpecSchema.safeParse(asSpec);
|
||||
return normalizedResult.success ? normalizedResult.data : null;
|
||||
}
|
||||
|
||||
if (Array.isArray(record.elements)) {
|
||||
const normalizedElements = record.elements
|
||||
.map((element) => normalizeElement(element))
|
||||
.filter((element): element is Record<string, unknown> => Boolean(element));
|
||||
|
||||
if (normalizedElements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asSpec = {
|
||||
specVersion: '1',
|
||||
elements: normalizedElements,
|
||||
};
|
||||
const normalizedResult = assistantPanelSpecSchema.safeParse(asSpec);
|
||||
return normalizedResult.success ? normalizedResult.data : null;
|
||||
}
|
||||
|
||||
const normalizedElement = normalizeElement(record);
|
||||
if (!normalizedElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asSpec = {
|
||||
specVersion: '1',
|
||||
elements: [normalizedElement],
|
||||
};
|
||||
const normalizedResult = assistantPanelSpecSchema.safeParse(asSpec);
|
||||
return normalizedResult.success ? normalizedResult.data : null;
|
||||
}
|
||||
|
||||
function parseSpecCandidate(raw: string): AssistantPanelSpec | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return normalizeCandidate(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractAssistantPanelSpec(message: string): AssistantPanelSpec | null {
|
||||
return extractAssistantResponseContent(message).panelSpec;
|
||||
}
|
||||
|
||||
export function extractAssistantResponseContent(message: string): AssistantResponseContent {
|
||||
const trimmed = message.trim();
|
||||
|
||||
const fencedMatches = [...trimmed.matchAll(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/gi)];
|
||||
for (const match of fencedMatches) {
|
||||
const candidate = match[1]?.trim();
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseSpecCandidate(candidate);
|
||||
if (parsed) {
|
||||
const displayText = trimmed.replace(match[0], '').trim();
|
||||
return {
|
||||
displayText,
|
||||
panelSpec: parsed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const parsedWholeMessage = parseSpecCandidate(trimmed);
|
||||
let displayText = parsedWholeMessage ? '' : trimmed;
|
||||
|
||||
if (parsedWholeMessage) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
if (parsed.protocolVersion === '2.0' && typeof parsed.assistantText === 'string') {
|
||||
displayText = parsed.assistantText;
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayText,
|
||||
panelSpec: parsedWholeMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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; envelope?: ProtocolResponseEnvelope; protocolVersion?: '2.0'; traceId?: string; warnings?: string[]; error?: string } | null | undefined>;
|
||||
) => Promise<{ success: boolean; message?: string; error?: string } | null | undefined>;
|
||||
}
|
||||
|
||||
export interface SendMessageMetadata {
|
||||
@@ -29,10 +27,6 @@ export interface SendConversationMessageInput {
|
||||
export interface SendConversationMessageResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
envelope?: ProtocolResponseEnvelope;
|
||||
protocolVersion?: '2.0';
|
||||
traceId?: string;
|
||||
warnings?: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -75,9 +69,5 @@ export async function sendConversationMessage(
|
||||
return {
|
||||
success: true,
|
||||
message: result.message || '',
|
||||
envelope: result.envelope,
|
||||
protocolVersion: result.protocolVersion,
|
||||
traceId: result.traceId,
|
||||
warnings: result.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { ProtocolResponseEnvelope } from '../types/electron';
|
||||
|
||||
export type ActionPolicyLevel = 'silent' | 'confirm' | 'danger';
|
||||
|
||||
export function buildActionPoliciesFromEnvelope(
|
||||
envelope: Pick<ProtocolResponseEnvelope, 'actions' | 'needsInput'>,
|
||||
): Record<string, ActionPolicyLevel> {
|
||||
const policies = envelope.actions.reduce<Record<string, ActionPolicyLevel>>((accumulator, action) => {
|
||||
accumulator[action.action] = action.policy;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
if (envelope.needsInput.required && envelope.needsInput.fields.length > 0 && !policies.submitNeedsInput) {
|
||||
policies.submitNeedsInput = 'confirm';
|
||||
}
|
||||
|
||||
return policies;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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),
|
||||
}];
|
||||
}
|
||||
Reference in New Issue
Block a user