-
-
-
+
+ {renderHeader()}
+ {senderVisible && (
+
+
+
+ )}
+ {footerNode ? footerNode : null}
);
diff --git a/web/src/components/Home/mock.ts b/web/src/components/Home/mock.ts
new file mode 100644
index 0000000..93f8a86
--- /dev/null
+++ b/web/src/components/Home/mock.ts
@@ -0,0 +1,145 @@
+import PhoneSvg from '@/assets/svg/staff/phone-icon.svg';
+import SuitCaseSvg from '@/assets/svg/staff/suitcase-icon.svg';
+import Locked from '@/assets//svg/staff/locked.svg';
+
+export interface EmployeeType {
+ id: number;
+ super_employee_id: string;
+ name: string;
+ avatar: string;
+ desc: string;
+ desc_items: string[];
+ status: 'ACTIVE' | 'COMING';
+ created_at: string;
+ updated_at: string;
+ agent_tools: { tool_name: string; tool_avatar: string }[];
+}
+
+export const listData: EmployeeType[] = [
+ {
+ id: 1,
+ super_employee_id: '1',
+ name: 'Travel agent',
+ avatar: Locked,
+ status: 'ACTIVE',
+ desc: 'Helps staff and businesses to quickly complete standardized travel requests and list processes',
+ desc_items: ['Smart planning of the trip', 'Cost budget management', 'Travel policy compliance'],
+ updated_at: '',
+ created_at: '',
+ agent_tools: [
+ {
+ tool_avatar: PhoneSvg,
+ tool_name: 'Cloud cell phone.',
+ },
+ {
+ tool_avatar: SuitCaseSvg,
+ tool_name: 'Code Compiler',
+ },
+ ],
+ },
+ {
+ id: 2,
+ super_employee_id: '2',
+ name: 'Logistics optimizer',
+ avatar: Locked,
+ status: 'ACTIVE',
+ desc: 'Call internal and external data sources and use scientific software for circuit optimization',
+ desc_items: ['Enterprise in-house database', 'External data sources', 'Sandbox Smart Body (AIProgramming)'],
+ updated_at: '',
+ created_at: '',
+ agent_tools: [
+ {
+ tool_avatar: PhoneSvg,
+ tool_name: 'Cloud cell phone.',
+ },
+ {
+ tool_avatar: SuitCaseSvg,
+ tool_name: 'Code Compiler',
+ },
+ ],
+ },
+ {
+ id: 3,
+ super_employee_id: '3',
+ name: 'Patent Engineer',
+ avatar: Locked,
+ status: 'ACTIVE',
+ desc: 'Helps staff and businesses to quickly complete standardized travel requests and list processes',
+ desc_items: ['Patent Retrieval', 'Patent Office database', 'AIWriting (including process)/(Framework)'],
+ updated_at: '',
+ created_at: '',
+ agent_tools: [
+ {
+ tool_avatar: PhoneSvg,
+ tool_name: 'Cloud cell phone.',
+ },
+ {
+ tool_avatar: SuitCaseSvg,
+ tool_name: 'Code Compiler',
+ },
+ ],
+ },
+ {
+ id: 4,
+ super_employee_id: '4',
+ name: 'Recruitment Commissioner',
+ avatar: Locked,
+ status: 'ACTIVE',
+ desc: 'Call internal and external data sources and use scientific software for circuit optimization',
+ desc_items: ['Enterprise in-house database', 'External data sources', 'Sandbox Smart Body (AIProgramming)'],
+ updated_at: '',
+ created_at: '',
+ agent_tools: [
+ {
+ tool_avatar: PhoneSvg,
+ tool_name: 'Cloud cell phone.',
+ },
+ {
+ tool_avatar: SuitCaseSvg,
+ tool_name: 'Code Compiler',
+ },
+ ],
+ },
+ {
+ id: 5,
+ super_employee_id: '5',
+ name: 'Assistant to the Chief of the Shop',
+ avatar: Locked,
+ status: 'ACTIVE',
+ desc: 'Call internal and external data sources and use scientific software for circuit optimization',
+ desc_items: ['Enterprise in-house database', 'External data sources', 'Sandbox Smart Body (AIProgramming)'],
+ updated_at: '',
+ created_at: '',
+ agent_tools: [
+ {
+ tool_avatar: PhoneSvg,
+ tool_name: 'Cloud cell phone.',
+ },
+ {
+ tool_avatar: SuitCaseSvg,
+ tool_name: 'Code Compiler',
+ },
+ ],
+ },
+ {
+ id: 6,
+ super_employee_id: '6',
+ name: 'Insurance agent',
+ avatar: Locked,
+ status: 'COMING',
+ desc: 'Call internal and external data sources and use scientific software for circuit optimization',
+ desc_items: ['Enterprise in-house database', 'External data sources', 'Sandbox Smart Body (AIProgramming)'],
+ updated_at: '',
+ created_at: '',
+ agent_tools: [
+ {
+ tool_avatar: PhoneSvg,
+ tool_name: 'Cloud cell phone.',
+ },
+ {
+ tool_avatar: SuitCaseSvg,
+ tool_name: 'Code Compiler',
+ },
+ ],
+ },
+];
diff --git a/web/src/components/Infra/CSVViewer/index.tsx b/web/src/components/Infra/CSVViewer/index.tsx
index 504b82e..5c05ddf 100644
--- a/web/src/components/Infra/CSVViewer/index.tsx
+++ b/web/src/components/Infra/CSVViewer/index.tsx
@@ -2,6 +2,7 @@ import React, { useMemo, useRef } from 'react';
import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useSize } from 'ahooks';
+import { getTranslation, useTranslation } from '@/hooks/useTranslation';
interface CSVViewerProps {
content: string;
@@ -11,42 +12,182 @@ interface CSVData {
[key: string]: string;
}
+interface CSVValidationResult {
+ isValid: boolean;
+ error?: string;
+}
+
+// Validate the CSV format.
+const validateCSV = (content: string): CSVValidationResult => {
+ if (!content || !content.trim()) {
+ return { isValid: false, error: getTranslation('csv.error.empty') };
+ }
+
+ try {
+ // Detect and remove a byte-order mark (BOM).
+ let processedContent = content;
+ if (content.charCodeAt(0) === 0xfeff) {
+ processedContent = content.slice(1);
+ } else if (content.charCodeAt(0) === 0xfffe) {
+ processedContent = content.slice(1);
+ }
+
+ const lines = processedContent.trim().split('\n');
+
+ // At least one header row is required.
+ if (lines.length < 1) {
+ return { isValid: false, error: getTranslation('csv.error.missing_header') };
+ }
+
+ // The header row must not be empty.
+ const headerLine = lines[0].trim();
+ if (!headerLine) {
+ return { isValid: false, error: getTranslation('csv.error.empty_header') };
+ }
+
+ // Count the header columns.
+ const headers: string[] = [];
+ let current = '';
+ let inQuotes = false;
+
+ for (let j = 0; j < headerLine.length; j++) {
+ const char = headerLine[j];
+ if (char === '"') {
+ inQuotes = !inQuotes;
+ } else if (char === ',' && !inQuotes) {
+ headers.push(current.trim());
+ current = '';
+ } else {
+ current += char;
+ }
+ }
+ headers.push(current.trim());
+
+ const expectedColumnCount = headers.length;
+
+ // At least one column is required.
+ if (expectedColumnCount === 0) {
+ return { isValid: false, error: getTranslation('csv.error.zero_columns') };
+ }
+
+ // Check for at least one valid data row.
+ let validDataRowCount = 0;
+ for (let i = 1; i < lines.length; i++) {
+ const line = lines[i].trim();
+ if (!line) continue;
+
+ // Count the columns in this row.
+ const values: string[] = [];
+ let cellCurrent = '';
+ let cellInQuotes = false;
+
+ for (let j = 0; j < line.length; j++) {
+ const char = line[j];
+ if (char === '"') {
+ cellInQuotes = !cellInQuotes;
+ } else if (char === ',' && !cellInQuotes) {
+ values.push(cellCurrent.trim());
+ cellCurrent = '';
+ } else {
+ cellCurrent += char;
+ }
+ }
+ values.push(cellCurrent.trim());
+
+ // Check that this row matches the header column count.
+ if (values.length !== expectedColumnCount) {
+ return {
+ isValid: false,
+ error: getTranslation('csv.error.column_mismatch', {
+ row: i + 1,
+ expected: expectedColumnCount,
+ actual: values.length,
+ }),
+ };
+ }
+
+ validDataRowCount++;
+ }
+
+ // At least one data row is required in addition to the header.
+ if (validDataRowCount === 0) {
+ return { isValid: false, error: getTranslation('csv.error.no_data') };
+ }
+
+ return { isValid: true };
+ } catch (error) {
+ return {
+ isValid: false,
+ error: getTranslation('csv.error.validation_failed', {
+ message: error instanceof Error ? error.message : getTranslation('csv.error.unknown'),
+ }),
+ };
+ }
+};
+
const CSVViewer: React.FC
= ({ content }) => {
const ref = useRef(null);
const size = useSize(ref);
+ const { t } = useTranslation();
+
+ const { data, columns, validationError } = useMemo(() => {
+ // Return empty data if content is empty or undefined (no error shown)
+ // This avoids displaying an error while content is loading.
+ if (!content || !content.trim()) {
+ return { data: [], columns: [], validationError: null };
+ }
- const { data, columns } = useMemo(() => {
- if (!content) {
- return { data: [], columns: [] };
+ // Validate the CSV before parsing it.
+ const validation = validateCSV(content);
+ if (!validation.isValid) {
+ console.warn('Invalid CSV:', validation.error);
+ return { data: [], columns: [], validationError: validation.error };
}
try {
- // detect and process BOM (byte order mark)
+ // Detect and remove a byte-order mark (BOM).
let processedContent = content;
if (content.charCodeAt(0) === 0xfeff) {
- // remove UTF-8 BOM
+ // Remove the UTF-8 BOM.
processedContent = content.slice(1);
} else if (content.charCodeAt(0) === 0xfffe) {
- // remove UTF-16 LE BOM
+ // Remove the UTF-16 LE BOM.
processedContent = content.slice(1);
}
- // parse CSV content
+ // Parse the CSV content.
const lines = processedContent.trim().split('\n');
- if (lines.length === 0) {
- return { data: [], columns: [] };
- }
- // parse headers
- const headers = lines[0].split(',').map((header) => header.trim().replace(/^["']|["']$/g, ''));
+ // Parse the header using the same logic as data rows.
+ const headerLine = lines[0];
+ const headers: string[] = [];
+ let headerCurrent = '';
+ let headerInQuotes = false;
- // parse data rows
+ for (let j = 0; j < headerLine.length; j++) {
+ const char = headerLine[j];
+ if (char === '"') {
+ headerInQuotes = !headerInQuotes;
+ } else if (char === ',' && !headerInQuotes) {
+ // headers.push(headerCurrent.trim().replace(/^["']|["']$/g, ''));
+ // Use a default name such as "Column_1" when a header is empty.
+ const finalHeader = headerCurrent.trim().replace(/^["']|["']$/g, '');
+ headers.push(finalHeader || `Column_${headers.length + 1}`);
+ headerCurrent = '';
+ } else {
+ headerCurrent += char;
+ }
+ }
+ const lastHeader = headerCurrent.trim().replace(/^["']|["']$/g, '');
+ headers.push(lastHeader || `Column_${headers.length + 1}`);
+
+ // Parse data rows.
const data: CSVData[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (!line.trim()) continue;
- // simple CSV parsing, handle commas inside quotes
+ // Handle commas inside quoted values.
const values: string[] = [];
let current = '';
let inQuotes = false;
@@ -62,7 +203,7 @@ const CSVViewer: React.FC = ({ content }) => {
current += char;
}
}
- values.push(current.trim()); // add the last value
+ values.push(current.trim()); // Add Last Value
const row: CSVData = {};
headers.forEach((header, index) => {
@@ -71,38 +212,65 @@ const CSVViewer: React.FC = ({ content }) => {
data.push(row);
}
- // generate table column configuration
+ // Generate the table column configuration.
const tableColumns: ColumnsType = headers.map((header) => ({
title: header,
dataIndex: header,
key: header,
ellipsis: true,
- render: (text: string) => (
-
- {text}
+ render: (text: unknown) => (
+
+ {String(text ?? '')}
),
}));
- return { data, columns: tableColumns };
+ return { data, columns: tableColumns, validationError: null };
} catch (error) {
console.error('CSV parsing error:', error);
- return { data: [], columns: [] };
+ return {
+ data: [],
+ columns: [],
+ validationError: error instanceof Error ? error.message : getTranslation('csv.error.unknown'),
+ };
}
}, [content]);
+ // Display an error when CSV validation fails.
+ if (validationError) {
+ return (
+
+
+
❌ {t('csv.format_error')}
+
{validationError}
+
+
+ );
+ }
+
+ // Show the empty state when there is no data.
+ if (data.length === 0) {
+ return (
+
+ );
+ }
+
return (
Object.values(record).join('-')}
columns={columns}
dataSource={data}
- pagination={{
- pageSize: 20,
- showSizeChanger: true,
- showQuickJumper: true,
+ virtual
+ pagination={false}
+ scroll={{
+ x: Math.max(columns.length * 160, size?.width ?? 0),
+ y: size?.height ? size.height - 60 : 600,
}}
- scroll={{ x: 'max-content', y: size?.height ? size.height - 100 : '100%' }}
className="h-full"
size="small"
bordered
diff --git a/web/src/components/Infra/CloudPhone/index.tsx b/web/src/components/Infra/CloudPhone/index.tsx
index b6a2f1c..0bdca14 100644
--- a/web/src/components/Infra/CloudPhone/index.tsx
+++ b/web/src/components/Infra/CloudPhone/index.tsx
@@ -5,41 +5,43 @@ import { isFunction, set } from 'lodash-es';
import phoneBgTopUrl from '@/assets/svg/phone-bg-top.svg';
import phoneBgBottomUrl from '@/assets/svg/phone-bg-bottom.svg';
import phoneHighlightUrl from '@/assets/png/phone-highlight.png';
+import { CloudPhoneAuthInfo } from '@/types';
+import { useTranslation } from '@/hooks/useTranslation';
-// dynamically load NzCp SDK
+// Dynamic Load NzCp SDK
const loadNzCpSDK = (): Promise => {
return new Promise((resolve, reject) => {
- // check if NzCp already exists
+ // Inspection NzCp Existence
if (typeof window !== 'undefined' && (window as any).NzCp) {
resolve();
return;
}
- // check if the script already exists
+ // Check if scripts are loaded
const existingScript = document.querySelector('script[src*="NZsdk.min.2.8.1.js"]');
if (existingScript) {
- // if the script already exists, wait for it to load
+ // If script exists, wait to load it
existingScript.addEventListener('load', () => resolve());
existingScript.addEventListener('error', reject);
return;
}
- // dynamically create script tag
+ // Dynamically create script labels
const script = document.createElement('script');
script.src = '/NZsdk.min.2.8.1.js';
script.async = true;
script.onload = () => {
- // check if NzCp successfully loads
+ // Inspection NzCp Whether to load successfully
if (typeof window !== 'undefined' && (window as any).NzCp) {
resolve();
} else {
- reject(new Error('NzCp SDK load failed'));
+ reject(new Error('NzCp SDK Loading failed'));
}
};
script.onerror = () => {
- reject(new Error('NzCp SDK script load failed'));
+ reject(new Error('NzCp SDK Script Load Failed'));
};
document.head.appendChild(script);
@@ -51,10 +53,7 @@ interface CloudPhoneProps {
needHumanIntervention?: boolean;
phoneRender?: () => React.ReactNode;
onUnbindPhone?: () => void;
- accessKey?: string;
- accessSecretKey?: string;
- instanceNo?: string;
- userId?: string;
+ authInfo?: CloudPhoneAuthInfo;
}
const CloudPhone: React.FC = ({
@@ -62,33 +61,46 @@ const CloudPhone: React.FC = ({
needHumanIntervention = false,
phoneRender,
onUnbindPhone,
- accessKey,
- accessSecretKey,
- instanceNo,
- userId,
+ authInfo,
}) => {
+ const { t } = useTranslation();
const sdkIns = useRef(null);
+ const isStarting = useRef(false);
const [phoneErrorCode, setPhoneErrorCode] = useState(0);
+ const [loading, setLoading] = useState(true);
const [needHighlight, setNeedHighlight] = useState(needHumanIntervention);
+ const {
+ user_id: userId,
+ instance_no: instanceNo,
+ access_key: accessKey,
+ access_secret_key: accessSecretKey,
+ } = authInfo || {};
const handlePhoneStart = useCallback(async () => {
+ if (isStarting.current || !userId || !instanceNo) return;
+ isStarting.current = true;
+ setLoading(true);
+
try {
- // dynamically load NzCp SDK
+ // Dynamic Load NzCp SDK
try {
await loadNzCpSDK();
} catch (error) {
- console.error('NzCp SDK fail to load:', error);
- message.error('SDK fail to load');
+ console.error('[CloudPhone] NzCp SDK fail to load:', error);
+ message.error(t('cloudphone.sdk.load_failed'));
+ isStarting.current = false;
return;
}
- // check if NzCp exists
+ // Inspection NzCp Existence
if (typeof window === 'undefined' || !(window as any).NzCp) {
- console.error('NzCp not found');
- message.error('SDK not found');
+ console.error('[CloudPhone] NzCp not found');
+ message.error(t('cloudphone.sdk.not_found'));
+ isStarting.current = false;
return;
}
+ console.info('Starting CloudPhone with:', { userId, instanceNo });
sdkIns.current = new (window as any).NzCp();
const param = {
userId,
@@ -97,34 +109,48 @@ const CloudPhone: React.FC = ({
isShowPausedDialog: false,
};
const callbacks = {
- onInitFail: (code) => {
- console.info('Cloud phone initialization failed:' + code);
+ onInitFail: (code: any) => {
+ console.error('[CloudPhone] The initialization of the cloud phone failed.:', code);
+ setPhoneErrorCode(code);
+ setLoading(false);
+ isStarting.current = false;
+ },
+ onStartFail: (code: any) => {
+ console.error('[CloudPhone] Cloud phone link failed:', code);
+ setPhoneErrorCode(code);
+ setLoading(false);
+ isStarting.current = false;
},
- onStartFail: (code) => {
- console.info('Cloud phone link failed:' + code);
+ onStartSuccess: () => {
+ console.info('[CloudPhone] Cloud cell link successful.');
+ setLoading(false);
+ isStarting.current = false;
},
- // onStartSuccess: () => {
- // // 链接成功
- // setLoading(false);
- // },
- onError: (code) => {
+ onError: (code: any) => {
setPhoneErrorCode(code);
- console.info('Cloud phone error:' + code);
+ console.error('[CloudPhone] The cloud phone is wrong.:', code);
+ setLoading(false);
+ isStarting.current = false;
},
};
const initRet = sdkIns.current.init(param, callbacks);
if (!initRet) {
- console.info('Cloud phone initialization failed');
+ console.error('[CloudPhone] Cloud cell phone. init Back false');
+ setLoading(false);
+ isStarting.current = false;
return;
}
sdkIns.current.start(accessKey, accessSecretKey);
} catch (error) {
console.info(error);
+ setLoading(false);
+ isStarting.current = false;
}
- }, [accessKey, accessSecretKey, instanceNo, userId]);
+ }, [accessKey, accessSecretKey, instanceNo, t, userId]);
const handlePhoneStop = () => {
if (sdkIns.current) {
+ console.info('[CloudPhone] Destroying SDK instance');
sdkIns.current?.destroy?.();
sdkIns.current = null;
}
@@ -156,7 +182,7 @@ const CloudPhone: React.FC = ({
return () => {
handlePhoneStop();
};
- }, [handlePhoneStart, phoneRender]);
+ }, [userId, instanceNo, phoneRender]);
useEffect(() => {
if (needHumanIntervention) {
@@ -171,7 +197,7 @@ const CloudPhone: React.FC = ({
}`}
>
{disabled &&
}
- {/* highlight */}
+ {/* Highlight. */}
{needHighlight && (
= ({
onMouseEnter={() => setNeedHighlight(false)}
>
)}
- {/* top background */}
+ {/* Top background. */}
- {/* bottom background */}
+ {/* Bottom background. */}
= ({
- {/* cloud phone */}
-
+ {/* Cloud phone. */}
+
{isFunction(phoneRender) ? (
phoneRender()
) : (
<>
-
+ {loading &&
}
{phoneErrorCode !== 0 && (
Error Code {phoneErrorCode}
)}
diff --git a/web/src/components/Infra/Code/index.tsx b/web/src/components/Infra/Code/index.tsx
index 035a884..80d158f 100644
--- a/web/src/components/Infra/Code/index.tsx
+++ b/web/src/components/Infra/Code/index.tsx
@@ -1,6 +1,7 @@
import { useTranslation } from '@/hooks/useTranslation';
import Editor, { DiffEditor } from '@monaco-editor/react';
import { Radio } from 'antd';
+import { isObject, isString } from 'lodash-es';
import React, { useState } from 'react';
@@ -9,93 +10,101 @@ interface CodeProps {
code?: string;
isDiff?: boolean;
/**
- * all languages supported by Monaco Editor
+ * Monaco Editor All supported languages
*/
language?:
- | 'abap'
- | 'apex'
- | 'azcli'
- | 'bat'
- | 'bicep'
- | 'c'
- | 'cameligo'
- | 'clojure'
- | 'coffeescript'
- | 'cpp'
- | 'csharp'
- | 'csp'
- | 'css'
- | 'dart'
- | 'dockerfile'
- | 'ecl'
- | 'elixir'
- | 'fsharp'
- | 'go'
- | 'graphql'
- | 'handlebars'
- | 'hcl'
- | 'html'
- | 'ini'
- | 'java'
- | 'javascript'
- | 'json'
- | 'julia'
- | 'kotlin'
- | 'less'
- | 'lexon'
- | 'lua'
- | 'markdown'
- | 'mips'
- | 'msdax'
- | 'mysql'
- | 'objective-c'
- | 'pascal'
- | 'pascaligo'
- | 'perl'
- | 'pgsql'
- | 'php'
- | 'plaintext'
- | 'postiats'
- | 'powerquery'
- | 'powershell'
- | 'proto'
- | 'pug'
- | 'python'
- | 'qsharp'
- | 'r'
- | 'razor'
- | 'redis'
- | 'redshift'
- | 'restructuredtext'
- | 'ruby'
- | 'rust'
- | 'sb'
- | 'scala'
- | 'scheme'
- | 'scss'
- | 'shell'
- | 'sol'
- | 'aes'
- | 'sparql'
- | 'sql'
- | 'st'
- | 'swift'
- | 'systemverilog'
- | 'tcl'
- | 'twig'
- | 'typescript'
- | 'vb'
- | 'xml'
- | 'yaml';
+ | 'abap'
+ | 'apex'
+ | 'azcli'
+ | 'bat'
+ | 'bicep'
+ | 'c'
+ | 'cameligo'
+ | 'clojure'
+ | 'coffeescript'
+ | 'cpp'
+ | 'csharp'
+ | 'csp'
+ | 'css'
+ | 'dart'
+ | 'dockerfile'
+ | 'ecl'
+ | 'elixir'
+ | 'fsharp'
+ | 'go'
+ | 'graphql'
+ | 'handlebars'
+ | 'hcl'
+ | 'html'
+ | 'ini'
+ | 'java'
+ | 'javascript'
+ | 'json'
+ | 'julia'
+ | 'kotlin'
+ | 'less'
+ | 'lexon'
+ | 'lua'
+ | 'markdown'
+ | 'mips'
+ | 'msdax'
+ | 'mysql'
+ | 'objective-c'
+ | 'pascal'
+ | 'pascaligo'
+ | 'perl'
+ | 'pgsql'
+ | 'php'
+ | 'plaintext'
+ | 'postiats'
+ | 'powerquery'
+ | 'powershell'
+ | 'proto'
+ | 'pug'
+ | 'python'
+ | 'qsharp'
+ | 'r'
+ | 'razor'
+ | 'redis'
+ | 'redshift'
+ | 'restructuredtext'
+ | 'ruby'
+ | 'rust'
+ | 'sb'
+ | 'scala'
+ | 'scheme'
+ | 'scss'
+ | 'shell'
+ | 'sol'
+ | 'aes'
+ | 'sparql'
+ | 'sql'
+ | 'st'
+ | 'swift'
+ | 'systemverilog'
+ | 'tcl'
+ | 'twig'
+ | 'typescript'
+ | 'vb'
+ | 'xml'
+ | 'yaml';
}
/**
- * code display component, read-only mode
+ * Code Display Component,Read-only Mode
*/
const Code = ({ originalCode = '', code = '', language = 'plaintext', isDiff = true }: CodeProps) => {
const [mode, setMode] = useState<'diff' | 'old' | 'new'>('new');
const { t } = useTranslation();
+ // Process original and code
+ const safeOriginalCode = isString(originalCode)
+ ? originalCode
+ : isObject(originalCode)
+ ? JSON.stringify(originalCode)
+ : '';
+ const safeCode = isString(code) ? code : isObject(code) ? JSON.stringify(code) : '';
+
const editorProps = {
width: '100%',
height: '100%',
@@ -115,17 +124,17 @@ const Code = ({ originalCode = '', code = '', language = 'plaintext', isDiff = t
const renderCode = () => {
if (mode === 'diff') {
- return
;
+ return
;
} else if (mode === 'old') {
- return
;
+ return
;
} else if (mode === 'new') {
- return
;
+ return
;
}
};
if (isDiff) {
return (
-
+
{renderCode()}
setMode(e.target.value)} className="absolute top-2 right-2 z-[1000]">
{t('code.diff')}
@@ -135,7 +144,7 @@ const Code = ({ originalCode = '', code = '', language = 'plaintext', isDiff = t
);
}
- return
;
+ return
;
};
export default Code;
diff --git a/web/src/components/Infra/DynamicForm/index.tsx b/web/src/components/Infra/DynamicForm/index.tsx
index fd28bea..c0fa7c6 100644
--- a/web/src/components/Infra/DynamicForm/index.tsx
+++ b/web/src/components/Infra/DynamicForm/index.tsx
@@ -173,7 +173,7 @@ const DynamicFormRenderer: React.FC
= ({
};
return (
-
+
{schema.title &&
{schema.title} }
{schema.description &&
{schema.description}
}
diff --git a/web/src/components/Infra/FileContentRender/index.tsx b/web/src/components/Infra/FileContentRender/index.tsx
index 55087fb..105c477 100644
--- a/web/src/components/Infra/FileContentRender/index.tsx
+++ b/web/src/components/Infra/FileContentRender/index.tsx
@@ -3,41 +3,46 @@ import React, { useMemo, useState } from 'react';
import { useTranslation } from '@/hooks/useTranslation';
import Code from '../Code';
import Markdown from '../Markdown';
+import VirtualMarkdown from '../VirtualMarkdown';
import CSVViewer from '../CSVViewer';
+import { getContentStats } from '@/utils/markdownBlockParser';
+import { FILE_CONTENT_SUPPORT_CONTENT_TYPES, FILE_CONTENT_SUPPORT_FILE_TYPES } from '@/utils/filePreview';
interface FileContentRenderProps {
/**
- * file extension
+ * File Extension
*/
fileExtension?: string;
/**
- * content type (MIME type)
+ * Content Type (MIME type)
*/
contentType?: string;
/**
- * file content
+ * Contents of the document
*/
fileContent?: string;
/**
- * old file content
+ * Text of old documents
*/
oldFileContent?: string;
/**
- * whether it is a difference file
+ * Whether or not to be a discrepancy document
*/
isDiff?: boolean;
}
-// supported file types (based on extension)
-export const fileContentSupportFileTypes = ['html', 'md', 'csv'];
+// Supported file type (based on extension)
+export const fileContentSupportFileTypes = [...FILE_CONTENT_SUPPORT_FILE_TYPES];
-// supported content types (based on MIME type)
-export const fileContentSupportContentTypes = ['text/html', 'text/markdown', 'text/csv'];
+// Type of content supported (based on MIME type)
+export const fileContentSupportContentTypes = [...FILE_CONTENT_SUPPORT_CONTENT_TYPES];
const fileTypeToLanguage = {
+ html: 'html',
+ htm: 'html',
md: 'markdown',
markdown: 'markdown',
py: 'python',
@@ -57,6 +62,22 @@ const fileTypeToLanguage = {
sql: 'sql',
};
+const srcDocBaseTag = '
';
+
+const injectSrcDocBaseTag = (html: string) => {
+ if (!html) return html;
+
+ if (/]*)?>/i.test(html)) {
+ return html.replace(/]*)?>/i, (match) => `${match}${srcDocBaseTag}`);
+ }
+
+ if (/]*)?>/i.test(html)) {
+ return html.replace(/]*)?>/i, (match) => `${match}${srcDocBaseTag}`);
+ }
+
+ return `${srcDocBaseTag}${html}`;
+};
+
const FileContentRender: React.FC
= ({
fileExtension = '',
contentType = '',
@@ -67,7 +88,7 @@ const FileContentRender: React.FC = ({
const [previewType, setPreviewType] = useState<'preview' | 'raw'>('preview');
const { t } = useTranslation();
- // extract file type from contentType
+ // From contentType Type of file to extract from
const getTypeFromContentType = (ct: string) => {
if (ct.includes('text/markdown') || ct.includes('markdown')) return 'md';
if (ct.includes('text/html') || ct.includes('html')) return 'html';
@@ -81,11 +102,29 @@ const FileContentRender: React.FC = ({
const language =
(fileTypeToLanguage[detectedType as keyof typeof fileTypeToLanguage] as any) || detectedType || 'plaintext';
+ // Check if virtual scroll is needed (only for Markdown)
+ const markdownStats = useMemo(() => {
+ if (detectedType === 'md' && fileContent) {
+ return getContentStats(fileContent);
+ }
+ return null;
+ }, [detectedType, fileContent]);
+
+ const htmlPreviewContent = useMemo(() => {
+ if (!['html', 'htm'].includes(detectedType)) return fileContent;
+
+ return injectSrcDocBaseTag(fileContent);
+ }, [detectedType, fileContent]);
+
const renderPreview = () => {
- if (detectedType === 'html') {
- return ;
+ if (['html', 'htm'].includes(detectedType)) {
+ return ;
}
if (detectedType === 'md') {
+ // Select render by file size
+ if (markdownStats?.shouldUseVirtual) {
+ return ;
+ }
return ;
}
if (detectedType === 'csv') {
@@ -93,7 +132,7 @@ const FileContentRender: React.FC = ({
}
};
- // check if supported preview mode (based on detected type)
+ // Check if preview mode is supported (based on detected type)
const supportsPreview =
fileContentSupportFileTypes.includes(detectedType) || fileContentSupportContentTypes.includes(contentType);
@@ -105,11 +144,11 @@ const FileContentRender: React.FC = ({
setPreviewType(e.target.value)}>
- {t('code.preview')}
{t('code.raw')}
+ {t('code.preview')}
-
+
{previewType === 'preview' ? (
renderPreview()
) : (
diff --git a/web/src/components/Infra/FileReader/UnsupportedFilePreview.tsx b/web/src/components/Infra/FileReader/UnsupportedFilePreview.tsx
new file mode 100644
index 0000000..8f934dc
--- /dev/null
+++ b/web/src/components/Infra/FileReader/UnsupportedFilePreview.tsx
@@ -0,0 +1,56 @@
+import React, { memo } from 'react';
+import { Button } from 'antd';
+import { useTranslation } from '@/hooks/useTranslation';
+import { downloadAttachment, getFileIcon } from '@/utils/file';
+import { getFileExtension } from '@/utils/parser';
+import { E2BFile } from '@/types';
+
+export interface UnsupportedFilePreviewProps {
+ url: string;
+ filename?: string;
+}
+
+const resolveDownloadFilename = (url: string, filename?: string): string => {
+ if (filename) {
+ return filename;
+ }
+
+ const extFromUrl = getFileExtension(url);
+ const urlPath = url.split('?')[0].split('#')[0];
+ const lastSegment = urlPath.substring(urlPath.lastIndexOf('/') + 1);
+
+ try {
+ const decoded = decodeURIComponent(lastSegment);
+ if (decoded) {
+ return decoded;
+ }
+ } catch {
+ if (lastSegment) {
+ return lastSegment;
+ }
+ }
+
+ return extFromUrl ? `download.${extFromUrl}` : 'download';
+};
+
+const UnsupportedFilePreview: React.FC
= ({ url, filename }) => {
+ const { t } = useTranslation();
+ const resolvedFilename = resolveDownloadFilename(url, filename);
+
+ const handleDownload = () => {
+ downloadAttachment({ url, filename: resolvedFilename } as E2BFile);
+ };
+
+ return (
+
+
{getFileIcon(resolvedFilename)}
+
{resolvedFilename}
+
{t('file.preview.unsupported')}
+
+ {t('attachment.download')}
+
+
+ );
+};
+
+export default memo(UnsupportedFilePreview);
diff --git a/web/src/components/Infra/FileReader/index.tsx b/web/src/components/Infra/FileReader/index.tsx
index 3b73e5b..57aeeee 100644
--- a/web/src/components/Infra/FileReader/index.tsx
+++ b/web/src/components/Infra/FileReader/index.tsx
@@ -1,135 +1,199 @@
import { useUrlContent } from '@/hooks/useUrlContent';
-import React, { FC, memo, useEffect, useState } from 'react';
-import FileContentRender, { fileContentSupportFileTypes } from '../FileContentRender';
+import React, { FC, memo, useEffect } from 'react';
+import FileContentRender from '../FileContentRender';
import ImageDetailRenderer from '@/registry/common/ImageDetailRenderer';
-import { getFileExtension } from '@/utils/parser';
import { Spin } from 'antd';
import OfficeFilePreview from './OfficeFilePreview';
-
-const prefix = 'https://view.officeapps.live.com/op/embed.aspx?src=';
-
-const officeFileExtensions = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'];
-// plaintext file extensions
-const plaintextFileExtensions = [
- 'txt',
- 'log',
- 'md',
- 'csv',
- 'json',
- 'xml',
- 'yaml',
- 'yml',
- 'ini',
- 'conf',
- 'cfg',
- 'properties',
- 'toml',
- 'ini',
- 'conf',
- 'cfg',
- 'properties',
- 'toml',
- 'ini',
- 'conf',
- 'cfg',
- 'properties',
- 'toml',
-];
-// code file extensions
-const codeFileExtensions = [
- 'js',
- 'jsx',
- 'ts',
- 'tsx',
- 'py',
- 'sh',
- 'bash',
- 'html',
- 'css',
- 'less',
- 'scss',
- 'json',
- 'xml',
- 'yaml',
- 'yml',
- 'ini',
- 'conf',
- 'cfg',
- 'properties',
- 'toml',
- 'ini',
- 'conf',
- 'cfg',
- 'properties',
- 'toml',
-];
+import UnsupportedFilePreview from './UnsupportedFilePreview';
+import { useTranslation } from '@/hooks/useTranslation';
+import { useAgentStore } from '@/store';
+import type { FilePreviewConfig } from '@/types';
+import {
+ canPreviewFile,
+ isImagePreviewFile,
+ isOfficePreviewFile,
+ isPdfPreviewFile,
+ isTextPreviewFile,
+ isVideoPreviewFile,
+ resolveOfficeFileTypeFromContentType,
+ resolvePreviewFileExtension,
+ shouldFetchFileContentForPreview,
+} from '@/utils/filePreview';
+
+const defaultOfficePreviewPrefix = 'https://view.officeapps.live.com/op/embed.aspx?src=';
export interface FileReaderProps {
url: string;
filename?: string;
contentType?: string;
+ filePreviewConfig?: FilePreviewConfig;
}
-/**
- * @param url file url, note: URL may not have a suffix, need to determine the file type through the type parameter
- * @param type file type, like text/csv
- */
-const FileReader: FC = ({ url, contentType, filename }) => {
- const { data, loading, fileType, blobUrl, error } = useUrlContent({ url, contentType });
+const getDefaultOfficePreviewUrl = (fileUrl: string): string => {
+ // Encode the complete file URL before passing it as the Office viewer src so signed queries remain intact.
+ return defaultOfficePreviewPrefix + encodeURIComponent(fileUrl);
+};
- const ext = getFileExtension(url) || getFileExtension(filename);
+const getOfficePreviewUrl = (fileUrl: string, officeConfig?: FilePreviewConfig['office']): string => {
+ const customPreviewUrlGetter = officeConfig?.getPreviewUrl;
+ if (!customPreviewUrlGetter) {
+ return getDefaultOfficePreviewUrl(fileUrl);
+ }
- const isImage =
- ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext) ||
- ['image/png', 'image/jpg', 'image/jpeg', 'image/gif', 'image/webp'].includes(contentType);
+ try {
+ return customPreviewUrlGetter(fileUrl) || getDefaultOfficePreviewUrl(fileUrl);
+ } catch (error) {
+ console.error('Failed to get custom office preview URL:', error);
+ return getDefaultOfficePreviewUrl(fileUrl);
+ }
+};
+
+const renderOfficeWithoutExtension = (
+ url: string,
+ contentType: string | undefined,
+ filePreviewConfig?: FilePreviewConfig,
+) => {
+ if (filePreviewConfig?.office?.getPreviewUrl) {
+ return (
+
+ );
+ }
+
+ const officeTypeFromMime = resolveOfficeFileTypeFromContentType(contentType);
+ if (officeTypeFromMime) {
+ return ;
+ }
+
+ return (
+
+ );
+};
+
+/**
+ * @param url File URL. It may not have an extension, so use type to determine the file format.
+ * @param type File MIME type, such as text/csv.
+ */
+const FileReader: FC = ({
+ url,
+ contentType,
+ filename,
+ filePreviewConfig: filePreviewConfigProp,
+}) => {
+ const { t } = useTranslation();
+ const { filePreviewConfig } = useAgentStore();
+ const resolvedFilePreviewConfig = filePreviewConfigProp ?? filePreviewConfig;
+
+ const previewOptions = { url, filename, contentType };
+ const ext = resolvePreviewFileExtension(previewOptions);
+ const isOfficeFile = isOfficePreviewFile(ext, contentType);
+ const shouldFetchContent = shouldFetchFileContentForPreview(previewOptions);
+
+ const { data, loading, fileType, blobUrl, error } = useUrlContent({
+ url: shouldFetchContent ? url : null,
+ contentType,
+ });
+
+ const resolvedPreviewOptions = { ...previewOptions, detectedFileType: fileType };
useEffect(() => {
- // when the component is unloaded, release the blob URL
+ // Revoke the blob URL when the component unmounts.
return () => {
if (blobUrl && blobUrl.startsWith('blob:')) {
URL.revokeObjectURL(blobUrl);
}
};
- }, []);
+ }, [blobUrl]);
- const renderUrl = (): string => {
- if (officeFileExtensions.some((ext) => url.endsWith(ext))) {
- return prefix + encodeURIComponent(url);
- }
+ if (isImagePreviewFile(ext, contentType)) {
+ return ;
+ }
- if (fileType === 'pdf') {
- return blobUrl;
- }
+ if (isOfficeFile && ext) {
+ return (
+
+ );
+ }
- return url;
- };
+ if (!ext && isOfficePreviewFile('', contentType)) {
+ return renderOfficeWithoutExtension(url, contentType, resolvedFilePreviewConfig);
+ }
- if (isImage) {
- return ;
+ if (shouldFetchContent && loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (shouldFetchContent && error) {
+ return (
+
+
+
❌ {t('file.load.failed')}
+
{error}
+
+
+ );
}
- if (
- fileContentSupportFileTypes.includes(ext) ||
- plaintextFileExtensions.includes(ext) ||
- codeFileExtensions.includes(ext)
- ) {
- return ;
+ if (!ext && !canPreviewFile(resolvedPreviewOptions)) {
+ return ;
}
- // preview link does not have a suffix, need to determine the file type through contentType
- if (!ext && ['docx', 'xlsx', 'pptx'].includes(fileType)) {
+ if (isTextPreviewFile(ext, contentType)) {
+ return (
+
+ );
+ }
+
+ if (!ext && isOfficePreviewFile('', contentType, fileType)) {
+ if (resolvedFilePreviewConfig?.office?.getPreviewUrl) {
+ return (
+
+ );
+ }
+
return ;
}
- if (loading) {
+ if (isPdfPreviewFile(ext, contentType, fileType)) {
return (
-
-
+
+ );
+ }
+
+ if (isVideoPreviewFile(ext, contentType)) {
+ return (
+
+
);
}
- return
;
+ return
;
};
export default memo(FileReader);
diff --git a/web/src/components/Infra/Icons/index.tsx b/web/src/components/Infra/Icons/index.tsx
new file mode 100644
index 0000000..43b4892
--- /dev/null
+++ b/web/src/components/Infra/Icons/index.tsx
@@ -0,0 +1,33 @@
+import React, { HTMLAttributes } from 'react';
+import Icon from '@ant-design/icons';
+import type { GetProps } from 'antd';
+
+import FileViewerCloseSvg from '@/assets/svg/fileviewer/close.svg?react';
+import FileViewerDownloadSvg from '@/assets/svg/fileviewer/download.svg?react';
+import FileViewerDownloadPngSvg from '@/assets/svg/fileviewer/download.png';
+import FileViewerZoomInSvg from '@/assets/svg/fileviewer/zoom-in.svg?react';
+import FileViewerZoomOutSvg from '@/assets/svg/fileviewer/zoom-out.svg?react';
+import FileViewerPptSvg from '@/assets/svg/fileviewer/ppt.svg?react';
+import FileViewerBatchDownloadSvg from '@/assets/svg/fileviewer/batch_download.svg?react';
+
+type CustomIconComponentProps = GetProps
;
+
+export const FileViewerCloseIcon = (props: CustomIconComponentProps) => (
+
+);
+export const FileViewerDownloadIcon = (props: CustomIconComponentProps) => (
+
+);
+export const FileViewerBatchDownloadIcon = (props: CustomIconComponentProps) => (
+
+);
+export const FileViewerZoomInIcon = (props: CustomIconComponentProps) => (
+
+);
+export const FileViewerZoomOutIcon = (props: CustomIconComponentProps) => (
+
+);
+export const FileViewerPptIcon = (props: CustomIconComponentProps) => ;
+export const FileViewerDownloadPngIcon = (props: HTMLAttributes) => (
+
+);
\ No newline at end of file
diff --git a/web/src/components/Infra/ImageViewer/index.tsx b/web/src/components/Infra/ImageViewer/index.tsx
index 91cde96..0eb3675 100644
--- a/web/src/components/Infra/ImageViewer/index.tsx
+++ b/web/src/components/Infra/ImageViewer/index.tsx
@@ -1,11 +1,11 @@
import React, { FC } from 'react';
import { Image, ImageProps } from 'antd';
-import type { GroupConsumerProps } from 'rc-image/lib/PreviewGroup';
+import { PreviewGroupProps } from 'antd/es/image/PreviewGroup';
interface ImageViewerProps extends ImageProps {
srcs?: string[];
group?: boolean;
- groupOptions?: GroupConsumerProps;
+ groupOptions?: PreviewGroupProps;
}
const ImageViewer: FC = ({ groupOptions, group, srcs, ...props }) => {
diff --git a/web/src/components/Infra/InputOutputDetailRenderer/index.tsx b/web/src/components/Infra/InputOutputDetailRenderer/index.tsx
index ab2d654..b3e66ca 100644
--- a/web/src/components/Infra/InputOutputDetailRenderer/index.tsx
+++ b/web/src/components/Infra/InputOutputDetailRenderer/index.tsx
@@ -31,7 +31,7 @@ const InputOutputDetailRenderer: React.FC = ({
{inputLabel}
-
@@ -43,7 +43,7 @@ const InputOutputDetailRenderer: React.FC = ({
{outputLabel}
-
diff --git a/web/src/components/Infra/Loading/index.tsx b/web/src/components/Infra/Loading/index.tsx
index e434ced..112e964 100644
--- a/web/src/components/Infra/Loading/index.tsx
+++ b/web/src/components/Infra/Loading/index.tsx
@@ -2,7 +2,7 @@ import LoadingSvg from '@/assets/svg/loading.svg?react';
import React from 'react';
-const Loading = ({ size = 16 }: { size?: number }) => {
+const Loading = ({ size = 18 }: { size?: number }) => {
return
;
};
diff --git a/web/src/components/Infra/Markdown/components/LiElement.tsx b/web/src/components/Infra/Markdown/components/LiElement.tsx
index 78959d4..27a2fc8 100644
--- a/web/src/components/Infra/Markdown/components/LiElement.tsx
+++ b/web/src/components/Infra/Markdown/components/LiElement.tsx
@@ -28,10 +28,10 @@ const LiElement: React.FC
= ({ children, className, ...props })
return () => observer.disconnect();
}, []);
- // check if it is a task list item
+ // Check if to be a task list entry
const isTaskListItem = className?.includes('task-list-item');
- // filter out non-standard DOM attributes
+ // Filter non-standard DOM Properties
const { ordered, ...domProps } = props;
return (
diff --git a/web/src/components/Infra/Markdown/components/LinkElement.tsx b/web/src/components/Infra/Markdown/components/LinkElement.tsx
index 252ffc3..d44deaf 100644
--- a/web/src/components/Infra/Markdown/components/LinkElement.tsx
+++ b/web/src/components/Infra/Markdown/components/LinkElement.tsx
@@ -1,9 +1,4 @@
-import React from "react";
-
-type LinkElementProps = React.DetailedHTMLProps<
- React.AnchorHTMLAttributes,
- HTMLAnchorElement
->;
+type LinkElementProps = React.DetailedHTMLProps, HTMLAnchorElement>;
const LinkElement: React.FC = ({ href, children }) => {
return (
diff --git a/web/src/components/Infra/Markdown/index.less b/web/src/components/Infra/Markdown/index.less
index bdaf6e2..13beed0 100644
--- a/web/src/components/Infra/Markdown/index.less
+++ b/web/src/components/Infra/Markdown/index.less
@@ -14,7 +14,7 @@
transform: translateY(-1px);
}
-.message-content-text {
+.agentx-content-text {
line-height: 28px;
font-size: 16px;
color: #34322d;
diff --git a/web/src/components/Infra/Markdown/index.tsx b/web/src/components/Infra/Markdown/index.tsx
index bd53ee6..a686a2a 100644
--- a/web/src/components/Infra/Markdown/index.tsx
+++ b/web/src/components/Infra/Markdown/index.tsx
@@ -38,15 +38,15 @@ const Markdown: React.FC = ({ content = '', className = '', proce
return content.replace(pattern, replacement);
};
- // a string that starts with $, non-alphabetic characters in the middle, and all digits, is recognized as a dollar symbol
+ //Here.$Start, end of non-English letter,The middle is full of numbers, and it's recognized as a dollar symbol.
const escapeDollarNumber = (text: string) => {
return text?.replace(/(\$\d+[^a-zA-Z])/g, '\\$1');
};
const escapeBrackets = (text: string) => {
- // handle code blocks and mathematical formulas
+ // Process code blocks and mathematical formulae
const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
- let result = text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
+ const result = text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
if (codeBlock) {
return codeBlock;
} else if (squareBracket) {
@@ -57,24 +57,49 @@ const Markdown: React.FC = ({ content = '', className = '', proce
return match;
});
- // handle punctuation after URL
- // add a space after the URL to prevent punctuation being included in the link
- result = result.replace(
- /(https?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)/g,
- '$1 ',
- );
+ // Processing URL The question of the subsequent punt (provisional note, recast re-ordering of the question, which leads to 2024-2025Figure of annual decoupling of urban housing prices.png --> 2024-2025 Figure of annual decoupling of urban housing prices.pngMultiple Spaces)
+ // Add a trailing space so punctuation is not included in the URL.
+ // result = result.replace(
+ // /(https?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)/g,
+ // '$1 ',
+ // );
return result;
};
+ const escapeNonHtmlAngleBrackets = (text: string) => {
+ const htmlTagPattern =
+ /^\/?\s*[a-z][a-z0-9-]*(?:\s+[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?$/;
+ const pattern = /(```[\s\S]*?```|`[^`\n]*`)|<([^>\n]+)>/g;
+
+ return text.replace(pattern, (match, codeBlock, angleContent) => {
+ if (codeBlock) {
+ return codeBlock;
+ }
+
+ if (!angleContent) {
+ return match;
+ }
+
+ const candidate = angleContent.trim();
+ if (candidate.startsWith('!') || candidate.startsWith('?') || htmlTagPattern.test(candidate)) {
+ return match;
+ }
+
+ return `<${angleContent}>`;
+ });
+ };
+
const escapedContent = useMemo(() => {
- return transformContent(escapeBrackets(escapeDollarNumber(content)));
+ return transformContent(escapeNonHtmlAngleBrackets(escapeBrackets(escapeDollarNumber(content))));
}, [content]);
+ if (!content) return null;
+
return (
@@ -90,7 +115,7 @@ const Markdown: React.FC
= ({ content = '', className = '', proce
]}
rehypePlugins={[
RehypeRaw as any,
- //RehypeSanitize: prevent script injection, default using github.com way. Reference: https://github.com/rehypejs/rehype-sanitize
+ //RehypeSanitize:Prevent script injection, default usegithub.comWorking methods.https://github.com/rehypejs/rehype-sanitize
[
RehypeSanitize,
{
diff --git a/web/src/components/Infra/Terminal/index.tsx b/web/src/components/Infra/Terminal/index.tsx
index 51ae7d8..91df152 100644
--- a/web/src/components/Infra/Terminal/index.tsx
+++ b/web/src/components/Infra/Terminal/index.tsx
@@ -6,15 +6,15 @@ interface TerminalProps {
}
/**
- * terminal output component, using ansi-to-react to process terminal output
+ * Terminal output component used ansi-to-react Process terminal output
*
* @example
* const terminalString = "\u001b[32mubuntu@sandbox:~ $\u001b[0m cd /home/ubuntu && mkdir -p chartmetric_analysis && cd chartmetric_analysis && mkdir -p data\n\n\u001b[32mubuntu@sandbox:~/chartmetric_analysis $\u001b[0m";
*
*/
const Terminal: React.FC = ({ content }) => {
- // first use Ansi to process, then split \n into multiple lines and insert
- // only render the first 1000 lines
+ // First. Ansi Handle it. \n Split Multiline Insert
+ // Before rendering only1000Okay.
const lines = content.split('\n').slice(0, 1000);
return (
diff --git a/web/src/components/Infra/ToolContainer/index.tsx b/web/src/components/Infra/ToolContainer/index.tsx
index b3d750a..17251fb 100644
--- a/web/src/components/Infra/ToolContainer/index.tsx
+++ b/web/src/components/Infra/ToolContainer/index.tsx
@@ -8,12 +8,16 @@ interface ToolContainerProps extends PropsWithChildren {
const ToolContainer: React.FC
= ({ icon, action, param }) => {
return (
-
- {icon &&
{icon}
}
-
-
{action}
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
{action}
{param && (
-
+
{param}
)}
diff --git a/web/src/components/Infra/VirtualMarkdown/index.tsx b/web/src/components/Infra/VirtualMarkdown/index.tsx
new file mode 100644
index 0000000..5a1a0f0
--- /dev/null
+++ b/web/src/components/Infra/VirtualMarkdown/index.tsx
@@ -0,0 +1,329 @@
+import React, { memo, useMemo, useState, useEffect } from 'react';
+import { Virtuoso } from 'react-virtuoso';
+import classNames from 'classnames';
+import ReactMarkdown from 'react-markdown';
+import RemarkGfm from 'remark-gfm';
+import RemarkBreaks from 'remark-breaks';
+import RemarkEmoji from 'remark-emoji';
+import RemarkImages from 'remark-images';
+import RemarkMath from 'remark-math';
+import RemarkDirective from 'remark-directive';
+import RemarkDirectiveRehype from 'remark-directive-rehype';
+import RehypeKatex from 'rehype-katex';
+import RehypeHighlight from 'rehype-highlight';
+import RehypeRaw from 'rehype-raw';
+import RehypeSanitize, { defaultSchema } from 'rehype-sanitize';
+import { Spin, Progress } from 'antd';
+
+import PreElement from '../Markdown/components/PreElement';
+import ImgElement from '../Markdown/components/ImgElement';
+import OlElement from '../Markdown/components/OlElement';
+import LiElement from '../Markdown/components/LiElement';
+import SectionElement from '../Markdown/components/SectionElement';
+import TableElement from '../Markdown/components/TableElement';
+import LinkElement from '../Markdown/components/LinkElement';
+import SupElement from '../Markdown/components/SupElement';
+
+import { MarkdownBlockParser, type MarkdownBlock, getContentStats } from '@/utils/markdownBlockParser';
+import { getWorkerManager, isWorkerSupported } from '@/utils/workerManager';
+import { useTranslation } from '@/hooks/useTranslation';
+
+import '../Markdown/index.less';
+
+interface VirtualMarkdownProps {
+ content?: string;
+ className?: string;
+ processing?: boolean;
+ onCopied?: (copy: string) => void;
+ /**
+ * Parsing strategy to force.
+ * When omitted, the strategy is selected automatically based on content size.
+ */
+ parseStrategy?: 'main-thread' | 'worker' | 'stream';
+}
+
+/**
+ * Virtual-scrolling Markdown component for large files.
+ * It avoids UI freezes caused by rendering all content at once.
+ */
+const VirtualMarkdown: React.FC
= ({
+ content = '',
+ className = '',
+ processing,
+ onCopied,
+ parseStrategy: forcedStrategy,
+}) => {
+ const [blocks, setBlocks] = useState([]);
+ const [isParsing, setIsParsing] = useState(true);
+ const [parseProgress, setParseProgress] = useState(0);
+ const [parseError, setParseError] = useState(null);
+ const { t } = useTranslation();
+
+ // Parse Markdown into blocks.
+ useEffect(() => {
+ if (!content) {
+ setBlocks([]);
+ setIsParsing(false);
+ return;
+ }
+
+ let isCancelled = false;
+
+ const parseContent = async () => {
+ setIsParsing(true);
+ setParseProgress(0);
+ setParseError(null);
+
+ try {
+ // Analyze the content and select a parsing strategy.
+ const stats = getContentStats(content);
+ const strategy = forcedStrategy || stats.parseStrategy;
+
+ let parsedBlocks: MarkdownBlock[];
+
+ if (strategy === 'main-thread') {
+ // Main-thread parsing (1-5 MB).
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ parsedBlocks = MarkdownBlockParser.parse(content);
+ if (!isCancelled) {
+ setParseProgress(100);
+ }
+ } else if (strategy === 'worker' && isWorkerSupported()) {
+ // Worker parsing (5-20 MB).
+ try {
+ const workerManager = getWorkerManager();
+ parsedBlocks = await workerManager.parse(content, (progress) => {
+ if (!isCancelled) {
+ setParseProgress(Math.round(progress * 100));
+ }
+ });
+ } catch (workerError) {
+ console.warn('Worker parsing failed, falling back to main thread:', workerError);
+ // Fall back to the main thread.
+ parsedBlocks = MarkdownBlockParser.parse(content);
+ }
+ } else if (strategy === 'stream' && isWorkerSupported()) {
+ // Streaming parsing (> 20 MB).
+ try {
+ const workerManager = getWorkerManager();
+ parsedBlocks = await workerManager.parseInChunks(content, 10000, (progress, currentBlocks) => {
+ if (!isCancelled) {
+ setParseProgress(Math.round(progress * 100));
+ // Display parsed blocks incrementally.
+ setBlocks([...currentBlocks]);
+ }
+ });
+ } catch (workerError) {
+ console.warn('Stream parsing failed, falling back to main thread:', workerError);
+ parsedBlocks = MarkdownBlockParser.parse(content);
+ }
+ } else {
+ // Fall back to the main thread by default.
+ parsedBlocks = MarkdownBlockParser.parse(content);
+ }
+
+ if (!isCancelled) {
+ setBlocks(parsedBlocks);
+ setIsParsing(false);
+ }
+ } catch (error) {
+ console.error('Failed to parse markdown:', error);
+ if (!isCancelled) {
+ setParseError(error instanceof Error ? error.message : t('markdown.parse.failed'));
+ // If parsing fails, render the entire content as one block.
+ setBlocks([
+ {
+ id: 'fallback-0',
+ type: 'root',
+ content,
+ startLine: 0,
+ endLine: content.split('\n').length - 1,
+ },
+ ]);
+ setIsParsing(false);
+ }
+ }
+ };
+
+ parseContent();
+
+ return () => {
+ isCancelled = true;
+ };
+ }, [content, forcedStrategy]);
+
+ // Match the content transformations used by the original Markdown component.
+ const transformContent = (text: string) => {
+ const pattern = /(\n|^)([-*]|[\d]\.)[^\n]*\n/g;
+ const replacement = `$1$&`;
+ return text.replace(pattern, replacement);
+ };
+
+ const escapeDollarNumber = (text: string) => {
+ return text?.replace(/(\$\d+[^a-zA-Z])/g, '\\$1');
+ };
+
+ const escapeBrackets = (text: string) => {
+ const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
+ const result = text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
+ if (codeBlock) {
+ return codeBlock;
+ } else if (squareBracket) {
+ return `$$${squareBracket}$$`;
+ } else if (roundBracket) {
+ return `$${roundBracket}$`;
+ }
+ return match;
+ });
+ return result;
+ };
+
+ const escapeNonHtmlAngleBrackets = (text: string) => {
+ const htmlTagPattern =
+ /^\/?\s*[a-z][a-z0-9-]*(?:\s+[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*\/?$/;
+ const pattern = /(```[\s\S]*?```|`[^`\n]*`)|<([^>\n]+)>/g;
+
+ return text.replace(pattern, (match, codeBlock, angleContent) => {
+ if (codeBlock) {
+ return codeBlock;
+ }
+
+ if (!angleContent) {
+ return match;
+ }
+
+ const candidate = angleContent.trim();
+ if (candidate.startsWith('!') || candidate.startsWith('?') || htmlTagPattern.test(candidate)) {
+ return match;
+ }
+
+ return `<${angleContent}>`;
+ });
+ };
+
+ // Markdown component configuration.
+ const markdownComponents = useMemo(
+ () => ({
+ pre: (code: any) => ,
+ img: (code: any) => ,
+ ol: (code: any) => ,
+ li: (code: any) => ,
+ section: (code: any) => ,
+ table: (code: any) => ,
+ a: (code: any) => ,
+ sup: (code: any) => ,
+ }),
+ [processing, onCopied],
+ );
+
+ const markdownPlugins = useMemo(
+ () => ({
+ remarkPlugins: [
+ [RemarkGfm, { singleTilde: false }] as any,
+ RemarkBreaks,
+ RemarkEmoji,
+ RemarkImages,
+ RemarkMath,
+ RemarkDirective,
+ RemarkDirectiveRehype,
+ ],
+ rehypePlugins: [
+ RehypeRaw as any,
+ [
+ RehypeSanitize,
+ {
+ ...defaultSchema,
+ attributes: {
+ ...defaultSchema.attributes,
+ '*': [['className', /^language-./, 'math-inline', 'math-display', 'katex']],
+ input: [...(defaultSchema.attributes?.input || []), 'type', 'checked', 'disabled'],
+ ol: [...(defaultSchema.attributes?.ol || []), 'start'],
+ },
+ },
+ ],
+ RehypeKatex,
+ [
+ RehypeHighlight,
+ {
+ ignoreMissing: true,
+ },
+ ],
+ ],
+ remarkRehypeOptions: {
+ footnoteLabel: 'Sources',
+ footnoteLabelTagName: 'div',
+ footnoteLabelProperties: {
+ className: 'footnote-label',
+ },
+ },
+ }),
+ [],
+ );
+
+ // Render one block.
+ const renderBlock = (index: number, block: MarkdownBlock) => {
+ const escapedContent = transformContent(
+ escapeNonHtmlAngleBrackets(escapeBrackets(escapeDollarNumber(block.content))),
+ );
+
+ return (
+
+
+ {escapedContent}
+
+
+ );
+ };
+
+ if (!content) return null;
+
+ if (isParsing && blocks.length === 0) {
+ return (
+
+
+
{t('markdown.parsing')}
+ {parseProgress > 0 &&
}
+
+ );
+ }
+
+ if (parseError) {
+ return (
+
+
+
{t('markdown.parse.failed')}
+
{parseError}
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default memo(VirtualMarkdown);
diff --git a/web/src/components/MessagesRender/index.d.ts b/web/src/components/MessagesRender/index.d.ts
new file mode 100644
index 0000000..8e9ac67
--- /dev/null
+++ b/web/src/components/MessagesRender/index.d.ts
@@ -0,0 +1,20 @@
+import React from 'react';
+import { MessageItem, MessageMetadata } from '@/types';
+import type { RenderMessageMetadata } from '@/types/agentx';
+
+export interface MessagesRenderProps = MessageMetadata> {
+ /**
+ * messages to render.
+ * @default []
+ */
+ messages: MessageItem[] | undefined;
+ /**
+ * Can not open message metadata Renderer. Use when not available AgentX store . The configuration or default rendering.
+ */
+ renderMessageMetadata?: RenderMessageMetadata;
+}
+
+export declare const MessagesRender: = MessageMetadata>(
+ props: MessagesRenderProps,
+) => React.ReactElement | null;
+export default MessagesRender;
diff --git a/web/src/components/MessagesRender/index.tsx b/web/src/components/MessagesRender/index.tsx
new file mode 100644
index 0000000..8803691
--- /dev/null
+++ b/web/src/components/MessagesRender/index.tsx
@@ -0,0 +1,132 @@
+import React, { Fragment } from 'react';
+import { message as antdMessage } from 'antd';
+import { CopyOutlined } from '@ant-design/icons';
+import { Bubble, Actions } from '@ant-design/x';
+import { useTranslation } from '@/hooks/useTranslation';
+import { isToolMessage } from '@/hooks/useChat/utils';
+import MessageAttachments from '@/components/Agent/Chatbot/MessageAttachments';
+import MessageMetadataRenderer from '@/components/Agent/Chatbot/MessageMetadataRenderer';
+import { Markdown } from '@/components/Infra';
+import Loading from '@/components/Infra/Loading';
+import MessageBrief from '@/registry/common/MessageBrief';
+import ToolRender from '@/components/Agent/Chatbot/ToolRender';
+import { MessageToolChunk } from '@/types';
+import type { MessagesRenderProps } from './index.d';
+import { resolveMessageMetadata } from '@/utils/messageMetadata';
+import { useAgentStore } from '@/store';
+
+const MessagesRenderComponent: React.FC = ({ messages, renderMessageMetadata: propRenderMessageMetadata }) => {
+ const { t } = useTranslation();
+ const { renderMessageMetadata: storeRenderMessageMetadata } = useAgentStore();
+ const renderMessageMetadata = propRenderMessageMetadata || storeRenderMessageMetadata;
+
+ if (!messages?.length) return null;
+
+ // const pipelineMessages = transformChunksToMessages(sessionChunks);
+
+ return messages.map((message, idx) => {
+ if (message.role === 'user') {
+ return message.messages.map((msg, idx) => {
+ const actionItems = msg.loading
+ ? []
+ : [
+ {
+ key: 'copy',
+ icon: ,
+ label: t('code.copy'),
+ },
+ ];
+ return (
+
+
{
+ if (info.key === 'copy') {
+ try {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(msg.content);
+ } else {
+ // Fallback: use the traditional rendering path.
+ const textArea = document.createElement('textarea');
+ textArea.value = msg.content;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand('copy');
+ document.body.removeChild(textArea);
+ }
+ antdMessage.success(t('code.copy.success'));
+ } catch (error) {
+ console.error('Copy failed:', error);
+ }
+ }
+ }}
+ />
+ {msg.loading ? : null}
+
+
+
+
+
+ }
+ />
+
+ );
+ });
+ } else if (message.role === 'assistant') {
+ const hasUserInput = message.messages.some((msg) => msg.type === 'user_input');
+ return (
+
+ {message.messages.map((msg, idx) => {
+ if (isToolMessage(msg as MessageToolChunk)) {
+ return (
+
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+ );
+ })}
+
+ }
+ />
+ );
+ }
+ });
+};
+
+export { MessagesRenderComponent as MessagesRender };
+export default MessagesRenderComponent;
diff --git a/web/src/components/WidgetRender/index.d.ts b/web/src/components/WidgetRender/index.d.ts
new file mode 100644
index 0000000..b4b7cce
--- /dev/null
+++ b/web/src/components/WidgetRender/index.d.ts
@@ -0,0 +1,52 @@
+import React, { Context } from 'react';
+import { JsxJson } from '@/types';
+import { WidgetRenderContextValue } from './index';
+
+// Re-export types from index.tsx to avoid duplication
+export type { WidgetRenderContextValue } from './index';
+
+export declare const WidgetRenderContext: Context
;
+
+export interface WidgetRenderError {
+ message: string;
+ stack?: string;
+ source: 'json' | 'jsx';
+}
+
+export interface WidgetRenderProps {
+ /**
+ * JSON schema for widget rendering.
+ * Accepts object or JSON string. When provided, takes precedence over JSX.
+ */
+ json?: JsxJson | string | null;
+ /**
+ * JSX string for widget rendering.
+ */
+ jsx?: string | null;
+ /**
+ * Data object injected into JSX when using jsx mode.
+ * Accepts object or loose JSON/JS object string.
+ */
+ data?: Record | string | null;
+ /**
+ * Custom component map extending built-in widgets.
+ */
+ customComponents?: Record>;
+ /**
+ * Placeholder shown when nothing to render.
+ */
+ placeholder?: React.ReactNode;
+ /**
+ * Callback fired when render error occurs.
+ */
+ onError?: (error: WidgetRenderError | null) => void;
+ /**
+ * Whether in preview mode (actions will not be executed)
+ * Use this in configuration pages where users preview widgets without executing actions
+ * @default false
+ */
+ isPreviewMode?: boolean;
+}
+
+export declare const WidgetRender: React.FC;
+export default WidgetRender;
diff --git a/web/src/components/WidgetRender/index.tsx b/web/src/components/WidgetRender/index.tsx
new file mode 100644
index 0000000..f9ff91b
--- /dev/null
+++ b/web/src/components/WidgetRender/index.tsx
@@ -0,0 +1,137 @@
+import React, { useEffect, useMemo, createContext } from 'react';
+import { jsxStringToJsx, jsonToJsx } from '@/engines/jsonToJsx';
+import { JsxJson } from '@/types';
+import { parseJsObject } from '@/utils/parseJsObject';
+import { AlertCircle } from 'lucide-react';
+import type { WidgetRenderProps } from './index.d';
+
+/**
+ * Context for WidgetRender to control preview mode and other render behaviors
+ */
+export interface WidgetRenderContextValue {
+ /**
+ * Whether in preview mode (actions will not be executed)
+ * @default false
+ */
+ isPreviewMode: boolean;
+}
+
+export const WidgetRenderContext = createContext({
+ isPreviewMode: false,
+});
+export interface WidgetRenderError {
+ message: string;
+ stack?: string;
+ source: 'json' | 'jsx';
+}
+
+interface WidgetRenderResult {
+ element: React.ReactElement | null;
+ error: WidgetRenderError | null;
+ source: 'json' | 'jsx' | null;
+}
+
+const toWidgetRenderError = (error: unknown, source: 'json' | 'jsx'): WidgetRenderError => {
+ const err = error as Error;
+ return {
+ message: err?.message || `Failed to render ${source.toUpperCase()} widget`,
+ stack: err?.stack,
+ source,
+ };
+};
+
+const ErrorDisplay: React.FC<{ error: WidgetRenderError | null }> = ({ error }) => {
+ if (!error) return null;
+
+ return (
+
+
+
{error.message}
+ {error.stack && (
+
+ )}
+
+ );
+};
+
+const PreviewContent: React.FC<{
+ element: React.ReactElement | null;
+ error: WidgetRenderError | null;
+ placeholder: React.ReactNode;
+}> = ({ element, error, placeholder }) => {
+ if (error) return ;
+ if (element) return element;
+ return placeholder;
+};
+
+const WidgetRenderComponent: React.FC = ({
+ json,
+ jsx,
+ data,
+ customComponents,
+ placeholder = 'Edit the code to preview',
+ onError,
+ isPreviewMode = false,
+}) => {
+ const renderResult = useMemo(() => {
+ const hasJson = typeof json === 'string' ? json.trim().length > 0 : json !== undefined && json !== null;
+
+ if (hasJson) {
+ try {
+ const parsedJson =
+ typeof json === 'string' ? (json.trim() ? (JSON.parse(json) as JsxJson) : null) : (json as JsxJson);
+
+ if (!parsedJson) {
+ return { element: null, error: null, source: 'json' };
+ }
+
+ const element = jsonToJsx(parsedJson, customComponents);
+ return { element, error: null, source: 'json' };
+ } catch (error) {
+ return { element: null, error: toWidgetRenderError(error, 'json'), source: 'json' };
+ }
+ }
+
+ const hasJsx = typeof jsx === 'string' ? jsx.trim().length > 0 : false;
+
+ if (hasJsx && typeof jsx === 'string') {
+ try {
+ const parsedData =
+ typeof data === 'string' ? (data.trim() ? parseJsObject(data) : undefined) : data || undefined;
+ const element = jsxStringToJsx(jsx, parsedData, customComponents);
+ return { element, error: null, source: 'jsx' };
+ } catch (error) {
+ return { element: null, error: toWidgetRenderError(error, 'jsx'), source: 'jsx' };
+ }
+ }
+
+ return { element: null, error: null, source: null };
+ }, [json, jsx, data, customComponents]);
+
+ useEffect(() => {
+ if (onError) {
+ onError(renderResult.error);
+ }
+ }, [renderResult.error, onError]);
+
+ const contextValue = useMemo(
+ () => ({
+ isPreviewMode,
+ }),
+ [isPreviewMode],
+ );
+
+ return (
+
+
+
+ );
+};
+
+export { WidgetRenderComponent as WidgetRender };
+export default WidgetRenderComponent;
diff --git a/web/src/components/widgets/BUTTON_GUIDE.md b/web/src/components/widgets/BUTTON_GUIDE.md
new file mode 100644
index 0000000..df65de6
--- /dev/null
+++ b/web/src/components/widgets/BUTTON_GUIDE.md
@@ -0,0 +1,79 @@
+# Button Component Guide
+
+The widget `Button` triggers an `ActionConfig` or submits the nearest widget form. It supports labels, Lucide icons, color tokens, visual variants, size presets, and loading state.
+
+## Basic Usage
+
+```tsx
+
+```
+
+A non-submit button without `onClickAction` is disabled automatically.
+
+## Props
+
+| Prop | Default | Description |
+| --- | --- | --- |
+| `label` | - | Text shown in the button |
+| `onClickAction` | - | Action executed on click |
+| `submit` | `false` | Uses `type="submit"` for the nearest form |
+| `iconStart` | - | Icon before the label |
+| `iconEnd` | - | Icon after the label |
+| `iconSize` | `md` | Icon size token |
+| `color` | `primary` | Button color token |
+| `variant` | `solid` | Visual variant token |
+| `style` | - | `primary` or `secondary` convenience preset |
+| `size` | `lg` | Overall size token |
+| `pill` | `true` | Enables the fully rounded shape |
+| `uniform` | `false` | Makes width equal to height |
+| `block` | `false` | Expands to the available width |
+| `disabled` | `false` | Disables interaction |
+| `collectFormData` | `true` | Merges widget form data into the action payload |
+
+Supported size tokens are `3xs`, `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, and `3xl`.
+
+## Icon-Only Button
+
+```tsx
+
+```
+
+## Form Submission
+
+```tsx
+
+```
+
+When both `submit` and `onClickAction` are provided, the action executor handles the click and prevents duplicate native submission.
+
+## Widget JSON
+
+```json
+{
+ "type": "Button",
+ "label": "Continue",
+ "color": "primary",
+ "variant": "solid",
+ "size": "lg",
+ "onClickAction": {
+ "type": "continue"
+ }
+}
+```
+
+The component exposes `data-color`, `data-variant`, `data-size`, `data-pill`, `data-uniform`, and `data-block` attributes consumed by `button.css`.
diff --git a/web/src/components/widgets/BUTTON_IMPLEMENTATION_SUMMARY.md b/web/src/components/widgets/BUTTON_IMPLEMENTATION_SUMMARY.md
new file mode 100644
index 0000000..e5c2d77
--- /dev/null
+++ b/web/src/components/widgets/BUTTON_IMPLEMENTATION_SUMMARY.md
@@ -0,0 +1,48 @@
+# Button Implementation Notes
+
+The widget button is split between behavior and presentation:
+
+- `Button.tsx` resolves props, executes actions, collects form data, renders loading state, and maps icon names to Lucide components.
+- `button.css` styles the component through semantic data attributes.
+- `iconMapping.ts` maps widget icon tokens to Lucide icon names.
+- `useActionExecutor` provides action execution and loading state.
+
+## State Model
+
+The button is disabled when any of these conditions is true:
+
+- `disabled` is set.
+- An action is running.
+- The button is not a submit button and has no `onClickAction`.
+
+While an action is running, the label and icons are replaced by a spinner. `aria-disabled`, native `disabled`, and `tabIndex` stay synchronized.
+
+## Style Contract
+
+Presentation is selected with data attributes:
+
+```html
+
+ ...
+
+```
+
+This keeps widget configuration independent from CSS class composition. New colors or variants should extend the existing token types and CSS selectors rather than add component-specific inline styles.
+
+## Accessibility
+
+- Use a label for commands that are not universally represented by their icon.
+- Icon-only buttons need an accessible name from their integration context.
+- Disabled buttons are removed from the tab order.
+- Decorative icons use `aria-hidden="true"`.
+- Loading state keeps the native button disabled to prevent duplicate actions.
+
+## Verification
+
+Exercise normal, hover, focus, active, disabled, and loading states for every affected color and variant. Verify icon-only, pill, uniform, block, submit, and action-backed buttons. Run `pnpm test` and `pnpm build` after behavior changes.
diff --git a/web/src/components/widgets/Badge.tsx b/web/src/components/widgets/Badge.tsx
new file mode 100644
index 0000000..310eeaa
--- /dev/null
+++ b/web/src/components/widgets/Badge.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import './badge.css';
+
+/**
+ * Badge component props
+ */
+export interface BadgeProps extends React.HTMLAttributes {
+ /**
+ * Text to display inside the badge.
+ * @example label="Success"
+ */
+ label: string;
+ /**
+ * Color of the badge; accepts a badge color token.
+ * @default "secondary"
+ */
+ color?: 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'discovery';
+ /**
+ * Visual style of the badge.
+ * @default "soft"
+ */
+ variant?: 'solid' | 'soft' | 'outline';
+ /**
+ * Size of the badge.
+ * @default "sm"
+ */
+ size?: 'sm' | 'md' | 'lg';
+ /**
+ * Determines if the badge should be a fully rounded pill shape.
+ * @default true
+ */
+ pill?: boolean;
+}
+
+/**
+ * Badge Component - Emphasize details with a status indicator.
+ *
+ * Quickly highlight statuses, metadata, and tags while keeping layouts compact.
+ * The badge renders as an inline-flex pill by default and exposes configuration
+ * through `color`, `variant`, `size`, and `pill`.
+ *
+ * Available colors:
+ * - `info`, `secondary`, `discovery`, `success`, `warning`, `danger`
+ *
+ * Available variants:
+ * - `soft` (default), `solid`, `outline`
+ *
+ * Available sizes:
+ * - `sm` (default), `md`, `lg`
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const Badge: React.FC = ({
+ label,
+ color = 'secondary',
+ variant = 'soft',
+ size = 'sm',
+ pill = true,
+ className,
+ ...props
+}) => {
+ return (
+
+ {label}
+
+ );
+};
+
+export default Badge;
diff --git a/web/src/components/widgets/Box.tsx b/web/src/components/widgets/Box.tsx
new file mode 100644
index 0000000..f50ed03
--- /dev/null
+++ b/web/src/components/widgets/Box.tsx
@@ -0,0 +1,215 @@
+import React from 'react';
+import { useStyleBuilder } from './styleBuilder';
+import { cn } from '@/lib/utils';
+import { StyleableProps } from './types';
+import { isNumber, isString } from 'lodash-es';
+
+interface BoxProps extends StyleableProps {
+ /**
+ * Container type
+ * @example
+ * ```json
+ * {
+ * "type": "Card",
+ * "container": "card"
+ * }
+ * ```
+ */
+ container?: 'card';
+ /**
+ * Card size
+ * @example
+ * ```json
+ * {
+ * "type": "Card",
+ * "size": "xs"
+ * }
+ * ```
+ */
+ 'data-size'?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+ /**
+ * Whether to flush the box to negate parent padding
+ * When true, generates data-w-flush attribute
+ * @default false
+ */
+ flush?: boolean;
+}
+
+/**
+ * Box Component - Generic flexible container
+ *
+ * A versatile layout component that serves as the foundation for building flexible layouts.
+ * Combines flexbox layout with spacing, sizing, and styling capabilities.
+ *
+ * @example
+ * ```tsx
+ *
+ * {children}
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Box",
+ * "padding": 4,
+ * "gap": 2,
+ * "direction": "row",
+ * "align": "center",
+ * "justify": "between",
+ * "background": "surface-tertiary",
+ * "radius": "md",
+ * "children": [...]
+ * }
+ * ```
+ */
+export const Box: React.FC = (props) => {
+ const {
+ component = 'box',
+ theme,
+ padding = 0,
+ direction = 'col',
+ width,
+ height,
+ size,
+ gap,
+ container,
+ 'data-size': dataSize,
+ flush = false,
+ flex,
+ } = props;
+
+ // use unified style builder
+ const { style: boxStyle, className: builtClassName } = useStyleBuilder({
+ ...props,
+ className: cn([props.className]),
+ });
+
+ // Calculate gutter values based on padding
+ // Convert padding to rem units (assuming padding is in spacing units: 1 unit = 0.25rem)
+ const getPaddingValue = (p: any): string => {
+ if (typeof p === 'number') {
+ return `${p * 0.25}rem`;
+ }
+ if (typeof p === 'string') {
+ // Try to parse as number first
+ const num = parseFloat(p);
+ if (!isNaN(num)) {
+ return `${num * 0.25}rem`;
+ }
+ return p;
+ }
+ return '0px';
+ };
+
+ // Handle object-based padding { x, y }
+ const parsePadding = (p: any) => {
+ if (typeof p === 'object' && p !== null && !Array.isArray(p)) {
+ const paddingObj = p as Record;
+ const toPaddingValue = (val: unknown) => (val !== undefined ? getPaddingValue(val) : undefined);
+
+ const inlineAxis =
+ toPaddingValue(paddingObj.inline) ??
+ toPaddingValue(paddingObj.x) ??
+ (direction === 'row' ? toPaddingValue(paddingObj.y) : undefined);
+ const blockAxis =
+ toPaddingValue(paddingObj.block) ??
+ toPaddingValue(paddingObj.y) ??
+ (direction === 'row' ? toPaddingValue(paddingObj.x) : undefined);
+
+ const blockStart = toPaddingValue(paddingObj.top) ?? blockAxis ?? '0px';
+ const blockEnd = toPaddingValue(paddingObj.bottom) ?? blockAxis ?? '0px';
+ const inlineStart = toPaddingValue(paddingObj.left) ?? inlineAxis ?? '0px';
+ const inlineEnd = toPaddingValue(paddingObj.right) ?? inlineAxis ?? '0px';
+
+ return {
+ blockStart,
+ blockEnd,
+ inlineStart,
+ inlineEnd,
+ };
+ }
+
+ // For non-object padding, use same value for all
+ const value = getPaddingValue(p);
+ return {
+ blockStart: value,
+ blockEnd: value,
+ inlineStart: value,
+ inlineEnd: value,
+ };
+ };
+
+ const { blockStart, blockEnd, inlineStart, inlineEnd } = parsePadding(padding);
+
+ // Check if gutter values are not zero
+ // Use regex to check if value is zero (0, 0px, 0rem, etc.)
+ const isZeroValue = (val: string) => /^0(px|rem)?$/.test(val);
+ const hasBlockStartGutter = blockStart && !isZeroValue(blockStart);
+ const hasBlockEndGutter = blockEnd && !isZeroValue(blockEnd);
+ const hasInlineStartGutter = inlineStart && !isZeroValue(inlineStart);
+ const hasInlineEndGutter = inlineEnd && !isZeroValue(inlineEnd);
+
+ const boxStyleWithGutter: React.CSSProperties = {
+ ...boxStyle,
+ // Set gutter variables for block and inline directions
+ ...(hasBlockStartGutter ? { '--w-box-gutter-block-start': blockStart } : {}),
+ ...(hasBlockEndGutter ? { '--w-box-gutter-block-end': blockEnd } : {}),
+ ...(hasInlineStartGutter ? { '--w-box-gutter-inline-start': inlineStart } : {}),
+ ...(hasInlineEndGutter ? { '--w-box-gutter-inline-end': inlineEnd } : {}),
+ // Set padding-block and padding-inline
+ ...(hasBlockStartGutter || hasBlockEndGutter
+ ? {
+ paddingBlock: blockStart === blockEnd ? blockStart : `${blockStart} ${blockEnd}`,
+ }
+ : {}),
+ ...(hasInlineStartGutter || hasInlineEndGutter
+ ? {
+ paddingInline: inlineStart === inlineEnd ? inlineStart : `${inlineStart} ${inlineEnd}`,
+ }
+ : {}),
+ ...(flex !== undefined
+ ? isString(flex)
+ ? { flex }
+ : isNumber(flex)
+ ? { flexShrink: flex, flexGrow: flex }
+ : { flex }
+ : {}),
+ } as React.CSSProperties;
+
+ // Determine if we have height/width (either directly or via size)
+ const hasHeight = height !== undefined || size !== undefined;
+ const hasWidth = width !== undefined || size !== undefined;
+
+ // Only use auto-spacing if gap is not explicitly set
+ const shouldUseAutoSpacing = gap === undefined;
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default Box;
diff --git a/web/src/components/widgets/Button.tsx b/web/src/components/widgets/Button.tsx
new file mode 100644
index 0000000..7065e40
--- /dev/null
+++ b/web/src/components/widgets/Button.tsx
@@ -0,0 +1,277 @@
+import React, { useMemo, createElement } from 'react';
+import { ButtonColor, ControlVariant, ControlSize, ButtonIcon } from '@/types';
+import { cn } from '@/lib/utils';
+import * as LucideIcons from 'lucide-react';
+import { buttonIconMap } from './iconMapping';
+import { ActionConfig } from './types';
+import { RotateCw } from 'lucide-react';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+/**
+ * Button component props
+ */
+export interface ButtonProps {
+ /**
+ * Configure the button as a submit button for the nearest form
+ * @default false
+ */
+ submit?: boolean;
+
+ /**
+ * Text to display inside the button
+ * @example label="Submit"
+ */
+ label?: string;
+
+ /**
+ * Action dispatched on click
+ * @example onClickAction={{ type: 'submit', payload: { form: 'login' } }}
+ */
+ onClickAction?: ActionConfig;
+
+ /**
+ * Icon shown before the label
+ * Can be used without a label to create an icon-only button
+ * @example iconStart="plus"
+ */
+ iconStart?: ButtonIcon;
+
+ /**
+ * Optional icon shown after the label
+ * @example iconEnd="arrow-right"
+ */
+ iconEnd?: ButtonIcon;
+
+ /**
+ * Convenience preset for button style
+ * @example style="primary"
+ */
+ style?: 'primary' | 'secondary';
+
+ /**
+ * Icon size preset
+ * @default "md"
+ * @example iconSize="lg"
+ */
+ iconSize?: ControlSize;
+
+ /**
+ * Color of the button; accepts a button color token
+ * @default "primary"
+ * @example color="success"
+ */
+ color?: ButtonColor;
+
+ /**
+ * Visual variant of the button; accepts a control variant token
+ * @default "solid"
+ * @example variant="outline"
+ */
+ variant?: ControlVariant;
+
+ /**
+ * Controls the overall size of the button
+ * Maps to specific height values:
+ * 3xs: 22px, 2xs: 24px, xs: 26px, sm: 28px, md: 32px,
+ * lg: 36px, xl: 40px, 2xl: 44px, 3xl: 48px
+ * @default "lg"
+ * @example size="xl"
+ */
+ size?: ControlSize;
+
+ /**
+ * Determines if the button should be a fully rounded pill shape
+ * @default true
+ * @example pill={false}
+ */
+ pill?: boolean;
+
+ /**
+ * Determines if the button should have matching width and height based on the size
+ * @default false
+ * @example uniform={true}
+ */
+ uniform?: boolean;
+
+ /**
+ * Extends button to 100% of available width
+ * @default false
+ * @example block={true}
+ */
+ block?: boolean;
+
+ /**
+ * Disables interactions and applies disabled styles
+ * Note: Button is also automatically disabled if onClickAction is not provided (unless submit={true})
+ * @default false
+ * @example disabled={true}
+ */
+ disabled?: boolean;
+
+ /**
+ * Whether to collect form data.
+ * @default true - Collect form data from the current widget scope.
+ */
+ collectFormData?: boolean;
+}
+
+/**
+ * Get lucide icon component by name
+ * @param iconName - Icon name in PascalCase (e.g., 'Plus', 'CheckCircle')
+ * @returns Icon component or null if not found
+ */
+function getLucideIcon(iconName: string): React.ComponentType | null {
+ const icon = (LucideIcons as Record)[iconName];
+ return (icon as React.ComponentType) || null;
+}
+
+/**
+ * Map icon sizes to lucide icon size classes
+ */
+const iconSizeMap: Record = {
+ '3xs': 'w-4 h-4',
+ '2xs': 'w-4 h-4',
+ xs: 'w-4 h-4',
+ sm: 'w-4 h-4',
+ md: 'w-5 h-5',
+ lg: 'w-5 h-5',
+ xl: 'w-6 h-6',
+ '2xl': 'w-6 h-6',
+ '3xl': 'w-6 h-6',
+};
+
+/**
+ * Button Component - For triggering actions
+ *
+ * A versatile button component with configurable colors, variants, sizes, and icons.
+ * Supports submit buttons, icon-only buttons, and various visual styles.
+ * Icons are provided by lucide-react.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Button",
+ * "label": "Click me",
+ * "color": "primary",
+ * "variant": "solid",
+ * "size": "lg"
+ * }
+ * ```
+ */
+export const Button: React.FC = ({
+ submit = false,
+ label,
+ onClickAction,
+ iconStart,
+ iconEnd,
+ style,
+ iconSize = 'md',
+ color = 'primary',
+ variant = 'solid',
+ size = 'lg',
+ pill = true,
+ uniform = false,
+ block = false,
+ disabled = false,
+ collectFormData = true,
+}) => {
+ const { executeAction, isLoading: actionLoading } = useActionExecutor({ collectFormData, label });
+ // Determine button type based on preset style or explicit color/variant
+
+ const buttonVariant = style ? (style === 'primary' ? 'solid' : 'outline') : variant;
+
+ // Auto-disable button if no onClickAction provided and not a submit button
+ const isDisabled = disabled || actionLoading || (!submit && !onClickAction);
+
+ // Get icon size class
+ const iconSizeClass = iconSizeMap[iconSize];
+
+ // Get lucide icons if provided
+ const startIcon = useMemo(() => {
+ if (!iconStart) return null;
+ const lucideIconName = buttonIconMap[iconStart];
+ return getLucideIcon(lucideIconName);
+ }, [iconStart]);
+
+ const endIcon = useMemo(() => {
+ if (!iconEnd) return null;
+ const lucideIconName = buttonIconMap[iconEnd];
+ return getLucideIcon(lucideIconName);
+ }, [iconEnd]);
+
+ // Handle click action
+ const handleClick = (e: React.MouseEvent) => {
+ if (isDisabled) {
+ e.preventDefault();
+ return;
+ }
+ console.log('onClickAction', onClickAction);
+ if (onClickAction) {
+ // Prevent the default form submission when a submit button defines onClickAction.
+ // This avoids triggering both the Button action and Form onSubmitAction.
+ if (submit) {
+ e.preventDefault();
+ }
+ e.persist?.();
+ executeAction(onClickAction, { event: e });
+ }
+ };
+
+ return (
+
+ {!actionLoading && (
+
+ {startIcon &&
+ createElement(startIcon as any, {
+ className: cn('w-button-icon w-button-icon-start', iconSizeClass),
+ 'aria-hidden': 'true',
+ })}
+ {label && {label} }
+ {endIcon &&
+ createElement(endIcon as any, {
+ className: cn('w-button-icon w-button-icon-end', iconSizeClass),
+ 'aria-hidden': 'true',
+ })}
+
+ )}
+
+ {actionLoading && (
+
+
+
+ )}
+
+ );
+};
+
+export default Button;
diff --git a/web/src/components/widgets/Caption.tsx b/web/src/components/widgets/Caption.tsx
new file mode 100644
index 0000000..7692acc
--- /dev/null
+++ b/web/src/components/widgets/Caption.tsx
@@ -0,0 +1,182 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import { resolveColorValue } from './colorUtils';
+
+/**
+ * Color configuration object for theme-aware colors
+ */
+export interface ColorConfig {
+ light?: string;
+ dark?: string;
+}
+
+/**
+ * Caption component props
+ */
+export interface CaptionProps {
+ /**
+ * Text content to display (required)
+ */
+ value: string;
+
+ /**
+ * Size of the caption text
+ * @default "md"
+ */
+ size?: 'sm' | 'md' | 'lg';
+
+ /**
+ * Font weight
+ * @default "normal"
+ */
+ weight?: 'normal' | 'medium' | 'semibold' | 'bold';
+
+ /**
+ * Text color
+ * Accepts text color token, primitive color token, CSS string, or theme-aware config
+ * Text color tokens: prose, primary, emphasis, secondary, tertiary, success, warning, danger
+ * @default "secondary"
+ */
+ color?: string | ColorConfig;
+
+ /**
+ * Horizontal text alignment
+ * @default "start"
+ */
+ textAlign?: 'start' | 'center' | 'end';
+
+ /**
+ * Truncate overflow with ellipsis
+ * @default false
+ */
+ truncate?: boolean;
+
+ /**
+ * Limit text to maximum number of lines (line clamp)
+ */
+ maxLines?: number;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Size to CSS class mapping
+ * Note: Font size is now controlled via CSS attribute selectors in widgets.css
+ */
+const sizeClasses: Record<'sm' | 'md' | 'lg', string> = {
+ sm: '',
+ md: '',
+ lg: '',
+};
+
+/**
+ * Weight to CSS class mapping
+ */
+const weightClasses: Record<'normal' | 'medium' | 'semibold' | 'bold', string> = {
+ normal: 'font-normal',
+ medium: 'font-medium',
+ semibold: 'font-semibold',
+ bold: 'font-bold',
+};
+
+/**
+ * Text alignment to CSS class mapping
+ */
+const textAlignClasses: Record<'start' | 'center' | 'end', string> = {
+ start: 'text-left',
+ center: 'text-center',
+ end: 'text-right',
+};
+
+/**
+ * Caption Component - Supplemental text for descriptions or hints
+ *
+ * A lightweight text component for displaying secondary information,
+ * help text, or captions below form fields and other content.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Caption",
+ * "value": "Supplemental text",
+ * "size": "md",
+ * "color": "secondary"
+ * }
+ * ```
+ */
+export const Caption: React.FC = ({
+ value,
+ size = 'md',
+ weight = 'normal',
+ color = 'secondary',
+ textAlign = 'start',
+ truncate = false,
+ maxLines,
+ className = '',
+ cssStyle,
+}) => {
+ // Resolve color value (text token, primitive token, or CSS color)
+ const colorResolver = typeof color === 'string' ? resolveColorValue(color) : {};
+ const colorValue = colorResolver.style || (typeof color === 'string' ? color : undefined);
+
+ const sizeClass = sizeClasses[size];
+ const weightClass = weightClasses[weight];
+ const alignClass = textAlignClasses[textAlign];
+
+ const wrapperClasses = cn(
+ 'w-caption',
+ sizeClass,
+ weightClass,
+ alignClass,
+ {
+ truncate: truncate,
+ 'line-clamp-2': maxLines === 2,
+ 'line-clamp-3': maxLines === 3,
+ 'line-clamp-4': maxLines === 4,
+ 'line-clamp-5': maxLines === 5,
+ 'line-clamp-6': maxLines === 6,
+ },
+ className,
+ );
+
+ const inlineStyles: React.CSSProperties = {
+ ...cssStyle,
+ ...(colorValue && { color: colorValue }),
+ };
+
+ return (
+
+ {value}
+
+ );
+};
+
+export default Caption;
diff --git a/web/src/components/widgets/Card.tsx b/web/src/components/widgets/Card.tsx
new file mode 100644
index 0000000..0fb311a
--- /dev/null
+++ b/web/src/components/widgets/Card.tsx
@@ -0,0 +1,108 @@
+import React, { ReactNode } from 'react';
+
+import { cn } from '@/lib/utils';
+import useStyleBuilder from './styleBuilder';
+import { ActionConfig, StyleableProps } from './types';
+import Box from './Box';
+import Button from './Button';
+import './card.css';
+
+export interface CardProps extends StyleableProps {
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+ confirm?: {
+ label: string;
+ action: ActionConfig;
+ };
+ cancel?: {
+ label: string;
+ action: ActionConfig;
+ };
+}
+
+/**
+ * Card Component - Root container for widgets
+ * configurable properties:
+ * - size: xs, sm, md, lg, xl
+ * - confirm: action button configuration
+ * - cancel: cancel button configuration
+ * unconfigurable properties:
+ * - border radius
+ * - border
+ * - shadow
+ */
+export const Card: React.FC = ({
+ children,
+ size = 'sm',
+ className,
+ theme,
+ padding = 4,
+ confirm,
+ cancel,
+ ...props
+}) => {
+ // declare card specific class name
+ const defaultClassName = cn(['w-card', 'overflow-hidden', 'w-max-full', 'border', className]);
+
+ const styles: React.CSSProperties = {
+ borderRadius: 20,
+ };
+
+ const { style: cardStyle, className: cardClassName } = useStyleBuilder({
+ ...props,
+ className: defaultClassName,
+ style: styles,
+ });
+
+ return (
+
+
+ {children}
+
+ {(confirm || cancel) && (
+
+ {confirm && (
+
+ )}
+ {cancel && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default Card;
diff --git a/web/src/components/widgets/Chart.tsx b/web/src/components/widgets/Chart.tsx
new file mode 100644
index 0000000..fa5bc24
--- /dev/null
+++ b/web/src/components/widgets/Chart.tsx
@@ -0,0 +1,470 @@
+import React, { useMemo } from 'react';
+import {
+ ResponsiveContainer,
+ ComposedChart,
+ BarChart,
+ LineChart,
+ AreaChart,
+ Bar,
+ Line,
+ Area,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer as RC,
+} from 'recharts';
+import { cn } from '@/lib/utils';
+
+/**
+ * Color configuration object for theme-aware colors
+ */
+export interface ColorConfig {
+ light?: string;
+ dark?: string;
+}
+
+/**
+ * Base series configuration
+ */
+export interface BaseSeries {
+ /**
+ * Key in each data row to read numeric values from
+ */
+ dataKey: string;
+
+ /**
+ * Label displayed in legends and tooltips
+ */
+ label?: string;
+
+ /**
+ * Color for the series (chart color token, primitive color, CSS string, or theme-aware config)
+ * Chart color tokens: blue, purple, orange, green, red, yellow, pink
+ */
+ color?: string | ColorConfig;
+
+ /**
+ * Stack ID to group series together
+ */
+ stack?: string;
+}
+
+/**
+ * Bar series configuration
+ */
+export interface BarSeries extends BaseSeries {
+ type: 'bar';
+}
+
+/**
+ * Line series configuration
+ */
+export interface LineSeries extends BaseSeries {
+ type: 'line';
+
+ /**
+ * Curve interpolation type
+ */
+ curveType?: 'linear' | 'natural' | 'monotone' | 'step' | 'basis' | 'bump';
+}
+
+/**
+ * Area series configuration
+ */
+export interface AreaSeries extends BaseSeries {
+ type: 'area';
+
+ /**
+ * Curve interpolation type
+ */
+ curveType?: 'linear' | 'natural' | 'monotone' | 'step' | 'basis' | 'bump';
+}
+
+/**
+ * Chart component props
+ */
+export interface ChartProps {
+ /**
+ * Tabular dataset; each object represents a data row (required)
+ */
+ data: Array;
+
+ /**
+ * Series definitions describing how to read and render data (required)
+ */
+ series: (BarSeries | LineSeries | AreaSeries)[];
+
+ /**
+ * X-axis configuration or the data key to use for the x-axis (required)
+ */
+ xAxis: string | { dataKey: string };
+
+ /**
+ * Show a left y-axis with tick labels
+ * @default false
+ */
+ showYAxis?: boolean;
+
+ /**
+ * Display a legend describing the series
+ * @default true
+ */
+ showLegend?: boolean;
+
+ /**
+ * Display a tooltip when hovering over data points
+ * @default true
+ */
+ showTooltip?: boolean;
+
+ /**
+ * Gap size in px between bars in the same category
+ */
+ barGap?: number;
+
+ /**
+ * Gap size in px between bar categories
+ */
+ barCategoryGap?: string;
+
+ /**
+ * Flex growth/shrink factor
+ */
+ flex?: string | number;
+
+ /**
+ * Explicit height
+ */
+ height?: string | number;
+
+ /**
+ * Explicit width
+ */
+ width?: string | number;
+
+ /**
+ * Shorthand for both width and height
+ */
+ size?: string | number;
+
+ /**
+ * Shorthand for both minWidth and minHeight
+ */
+ minSize?: string | number;
+
+ /**
+ * Shorthand for both maxWidth and maxHeight
+ */
+ maxSize?: string | number;
+
+ /**
+ * Minimum height constraint
+ */
+ minHeight?: string | number;
+
+ /**
+ * Minimum width constraint
+ */
+ minWidth?: string | number;
+
+ /**
+ * Maximum height constraint
+ */
+ maxHeight?: string | number;
+
+ /**
+ * Maximum width constraint
+ */
+ maxWidth?: string | number;
+
+ /**
+ * Aspect ratio (e.g., 16/9)
+ */
+ aspectRatio?: string | number;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Chart color token to CSS variable mapping
+ */
+const chartColorTokens: Record = {
+ blue: 'var(--blue-400)',
+ purple: 'var(--purple-400)',
+ orange: 'var(--orange-400)',
+ green: 'var(--green-400)',
+ red: 'var(--red-400)',
+ yellow: 'var(--yellow-400)',
+ pink: 'var(--pink-400)',
+};
+
+/**
+ * Get color value from token or config
+ */
+const getColorValue = (color: string | ColorConfig | undefined, defaultColor: string): string => {
+ if (!color) return defaultColor;
+ if (typeof color === 'string') {
+ return chartColorTokens[color] || color;
+ }
+ return color.light || defaultColor;
+};
+
+/**
+ * Get dimension value as string
+ */
+const getDimensionValue = (value: string | number | undefined): string | undefined => {
+ if (value === undefined) return undefined;
+ if (typeof value === 'number') return `${value}px`;
+ return value;
+};
+
+/**
+ * Default chart colors cycle
+ */
+const defaultColors = [
+ 'var(--blue-400)',
+ 'var(--purple-400)',
+ 'var(--orange-400)',
+ 'var(--green-400)',
+ 'var(--red-400)',
+ 'var(--yellow-400)',
+ 'var(--pink-400)',
+];
+
+/**
+ * Chart Component - Render simple bar, line, and area charts from tabular data
+ *
+ * A flexible charting component supporting multiple series and visualization types.
+ * Powered by Recharts for responsive, interactive charts.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const Chart: React.FC = ({
+ data,
+ series,
+ xAxis,
+ showYAxis = false,
+ showLegend = true,
+ showTooltip = true,
+ barGap,
+ barCategoryGap,
+ flex,
+ height,
+ width,
+ size,
+ minSize,
+ maxSize,
+ minHeight,
+ minWidth,
+ maxHeight,
+ maxWidth,
+ aspectRatio,
+ className = '',
+ cssStyle,
+}) => {
+ const xAxisKey = typeof xAxis === 'string' ? xAxis : xAxis.dataKey;
+
+ // Resolve dimensions
+ let finalHeight = height;
+ let finalWidth = width;
+ let finalMinHeight = minHeight;
+ let finalMinWidth = minWidth;
+ let finalMaxHeight = maxHeight;
+ let finalMaxWidth = maxWidth;
+
+ if (size !== undefined && !height && !width) {
+ finalHeight = size;
+ finalWidth = size;
+ }
+
+ if (minSize !== undefined && !minHeight && !minWidth) {
+ finalMinHeight = minSize;
+ finalMinWidth = minSize;
+ }
+
+ if (maxSize !== undefined && !maxHeight && !maxWidth) {
+ finalMaxHeight = maxSize;
+ finalMaxWidth = maxSize;
+ }
+
+ const wrapperClasses = cn('w-chart', className);
+
+ const inlineStyles: React.CSSProperties = {
+ ...cssStyle,
+ flex: flex,
+ height: getDimensionValue(finalHeight as number | string),
+ width: getDimensionValue(finalWidth as number | string),
+ minHeight: getDimensionValue(finalMinHeight as number | string),
+ minWidth: getDimensionValue(finalMinWidth as number | string),
+ maxHeight: getDimensionValue(finalMaxHeight as number | string),
+ maxWidth: getDimensionValue(finalMaxWidth as number | string),
+ aspectRatio: typeof aspectRatio === 'number' ? aspectRatio : aspectRatio,
+ position: 'relative',
+ };
+
+ // Determine if we need composed chart (mixed types)
+ const hasMultipleTypes = new Set(series.map((s) => s.type)).size > 1;
+
+ // Get series colors
+ const seriesWithColors = useMemo(() => {
+ return series.map((s, index) => ({
+ ...s,
+ color: getColorValue(s.color, defaultColors[index % defaultColors.length]),
+ }));
+ }, [series]);
+
+ // Render chart based on series types
+ const renderChart = () => {
+ if (hasMultipleTypes) {
+ // Use ComposedChart for mixed types
+ return (
+
+
+
+ {showYAxis && }
+ {showTooltip && }
+ {showLegend && }
+ {seriesWithColors.map((s) => {
+ const key = `${s.type}-${s.dataKey}`;
+ if (s.type === 'bar') {
+ return (
+
+ );
+ } else if (s.type === 'line') {
+ return (
+
+ );
+ } else if (s.type === 'area') {
+ return (
+
+ );
+ }
+ })}
+
+ );
+ }
+
+ // Single type chart optimization
+ const firstType = series[0]?.type;
+
+ if (firstType === 'bar') {
+ return (
+
+
+
+ {showYAxis && }
+ {showTooltip && }
+ {showLegend && }
+ {seriesWithColors.map((s, index) => (
+
+ ))}
+
+ );
+ } else if (firstType === 'line') {
+ return (
+
+
+
+ {showYAxis && }
+ {showTooltip && }
+ {showLegend && }
+ {seriesWithColors.map((s, index) => (
+
+ ))}
+
+ );
+ } else if (firstType === 'area') {
+ return (
+
+
+
+ {showYAxis && }
+ {showTooltip && }
+ {showLegend && }
+ {seriesWithColors.map((s, index) => (
+
+ ))}
+
+ );
+ }
+ };
+
+ return (
+
+
+ {renderChart()}
+
+
+ );
+};
+
+export default Chart;
diff --git a/web/src/components/widgets/Checkbox.tsx b/web/src/components/widgets/Checkbox.tsx
new file mode 100644
index 0000000..8360541
--- /dev/null
+++ b/web/src/components/widgets/Checkbox.tsx
@@ -0,0 +1,119 @@
+import React, { useMemo, useState } from 'react';
+import { Checkbox as AntdCheckbox, CheckboxChangeEvent } from 'antd';
+import { ActionConfig } from './types';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+/**
+ * Checkbox component props
+ */
+export interface CheckboxProps {
+ /**
+ * The name of the form control field (required)
+ * When the form is submitted, the value will be included in onSubmitAction payload
+ * Supports dot-separated paths: "myData.myFieldName" → payload.myData.myFieldName
+ */
+ name: string;
+
+ /**
+ * Optional label text rendered next to the checkbox
+ * @example label="I agree to the terms"
+ */
+ label?: string;
+
+ /**
+ * The initial checked state of the checkbox
+ * @default false
+ */
+ defaultChecked?: boolean;
+
+ /**
+ * Action dispatched when the checked state changes
+ * @example onChangeAction={{ type: 'checkboxChanged', payload: { field: 'accept' } }}
+ */
+ onChangeAction?: ActionConfig;
+
+ /**
+ * Disable interactions and apply disabled styles
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Mark the checkbox as required for form submission
+ * @default false
+ */
+ required?: boolean;
+}
+
+/**
+ * Checkbox Component - For binary selection
+ *
+ * A customizable checkbox component for toggling binary states with optional labels.
+ * Integrated with form submission and supports various styling options.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Checkbox",
+ * "name": "newsletter",
+ * "label": "Sign up for newsletter",
+ * "defaultChecked": false
+ * }
+ * ```
+ */
+export const Checkbox: React.FC = ({
+ name,
+ label,
+ defaultChecked = false,
+ onChangeAction,
+ disabled = false,
+ required = false,
+}) => {
+ const [checked, setChecked] = useState(defaultChecked);
+ const { executeAction } = useActionExecutor();
+
+ // Handle check change
+ const handleCheckedChange = (e: CheckboxChangeEvent) => {
+ const newChecked = e.target.checked;
+ setChecked(newChecked);
+
+ executeAction(onChangeAction, {
+ payload: {
+ checked: newChecked,
+ fieldName: name,
+ },
+ event: e.nativeEvent,
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default Checkbox;
diff --git a/web/src/components/widgets/Col.tsx b/web/src/components/widgets/Col.tsx
new file mode 100644
index 0000000..a74a331
--- /dev/null
+++ b/web/src/components/widgets/Col.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import Box from './Box';
+import { StyleableProps } from './types';
+
+/**
+ * Col component props - extends StyleableProps with vertical layout preset
+ */
+export type ColProps = StyleableProps;
+
+/**
+ * Col Component - Vertical flex layout container
+ *
+ * A convenience component that renders a Box with vertical (column) direction.
+ * Perfect for creating vertical stacks, menus, forms, or block-based content arrangements.
+ *
+ * Creates a flexbox container with flex-direction set to column, allowing you to
+ * stack children vertically with consistent spacing and alignment control.
+ *
+ * Layout Properties:
+ * - gap: Space between direct children (rem units or CSS string)
+ * - padding: Inner padding (rem units, CSS string, or padding object)
+ * - align: Cross-axis alignment - 'start', 'center', 'end', 'stretch'
+ * - justify: Main-axis distribution - 'start', 'center', 'end', 'between', 'around'
+ * - wrap: Flex wrap behavior - 'nowrap', 'wrap', 'wrap-reverse'
+ *
+ * Size Properties:
+ * - width: Explicit width (pixels or CSS string)
+ * - height: Explicit height (pixels or CSS string)
+ * - minWidth, maxWidth: Width constraints (pixels)
+ * - minHeight, maxHeight: Height constraints (pixels)
+ *
+ * Style Properties:
+ * - background: Background color token or CSS color
+ * - radius: Border radius preset - 'xs', 'sm', 'md', 'lg', 'xl', 'full'
+ * - flex: Flex growth/shrink factor
+ *
+ * HTML Properties:
+ * - children: Child components to render
+ * - className: Additional CSS class names
+ * - style: Inline CSS styles
+ *
+ * @example
+ * Vertical stack with title and description:
+ *
+ *
+ *
+ *
+ *
+ * @example
+ * Column with spacing and background:
+ *
+ * {children}
+ *
+ *
+ * @example
+ * Centered column with max-width constraint:
+ *
+ * {content}
+ *
+ *
+ * @example
+ * JSON configuration for vertical layout:
+ * {
+ * "type": "Col",
+ * "gap": 2,
+ * "padding": 4,
+ * "background": "surface-tertiary",
+ * "children": [
+ * { "type": "Title", "value": "Column title" },
+ * { "type": "Text", "value": "Supporting text" }
+ * ]
+ * }
+ */
+export const Col: React.FC = (props) => {
+ return ;
+};
+
+export default Col;
diff --git a/web/src/components/widgets/DatePicker.css b/web/src/components/widgets/DatePicker.css
new file mode 100644
index 0000000..1921353
--- /dev/null
+++ b/web/src/components/widgets/DatePicker.css
@@ -0,0 +1,34 @@
+[data-w-component='date-picker'] {
+ height: var(--select-control-size);
+ cursor: pointer;
+}
+
+[data-w-component='date-picker'] .ant-picker {
+ height: var(--select-control-size);
+ padding: 0 var(--select-control-gutter);
+ border-radius: var(--select-control-radius);
+ font-size: var(--select-control-font-size);
+ font-weight: var(--select-control-font-weight);
+ line-height: var(--font-text-md-line-height);
+}
+
+[data-w-component='date-picker'] .ant-picker .ant-picker-input > input {
+ font-size: var(--select-control-font-size);
+}
+
+[data-w-component='date-picker'][data-variant='solid'] .ant-picker-outlined {
+ border-color: transparent;
+}
+
+[data-w-component='date-picker'][data-variant='soft'] .ant-picker-outlined {
+ border-color: transparent;
+ background-color: var(--select-control-background-color);
+}
+
+[data-w-component='date-picker'][data-variant='ghost'] .ant-picker-outlined {
+ border-color: transparent;
+}
+[data-w-component='date-picker'][data-variant='ghost']:hover .ant-picker-outlined {
+ background-color: var(--select-control-background-color);
+}
+
diff --git a/web/src/components/widgets/DatePicker.tsx b/web/src/components/widgets/DatePicker.tsx
new file mode 100644
index 0000000..90054a8
--- /dev/null
+++ b/web/src/components/widgets/DatePicker.tsx
@@ -0,0 +1,222 @@
+import React, { useMemo, useState } from 'react';
+import dayjs, { Dayjs } from 'dayjs';
+import type { ControlVariant, ControlSize } from '@/types';
+import { DatePicker as AntdDatePicker } from 'antd';
+import './DatePicker.css';
+import { ActionConfig } from './types';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+/**
+ * DatePicker component props
+ */
+export interface DatePickerProps {
+ /**
+ * The name of the form control field (required)
+ * When the form is submitted, the value will be included in onSubmitAction payload
+ * Supports dot-separated paths: "myData.myFieldName" → payload.myData.myFieldName
+ */
+ name: string;
+
+ /**
+ * Action dispatched when the date value changes
+ * @example onChangeAction={{ type: 'dateChanged', payload: { date: '2024-01-31' } }}
+ */
+ onChangeAction?: ActionConfig;
+
+ /**
+ * Placeholder text shown when no date is selected
+ * @example placeholder="YYYY-MM-DD"
+ */
+ placeholder?: string;
+
+ /**
+ * Initial ISO date string
+ * @example defaultValue="2024-01-31"
+ */
+ defaultValue?: string;
+
+ /**
+ * Earliest selectable ISO date (inclusive)
+ * @example min="2024-01-01"
+ */
+ min?: string;
+
+ /**
+ * Latest selectable ISO date (inclusive)
+ * @example max="2024-12-31"
+ */
+ max?: string;
+
+ /**
+ * Visual variant of the datepicker control
+ * @default "outline"
+ */
+ variant?: ControlVariant;
+
+ /**
+ * Controls the size of the datepicker
+ * @default "md"
+ */
+ size?: ControlSize;
+
+ /**
+ * Preferred side to render the calendar
+ */
+ side?: 'top' | 'bottom' | 'left' | 'right';
+
+ /**
+ * Preferred alignment of the calendar relative to the datepicker control
+ * @default "center"
+ */
+ align?: 'start' | 'center' | 'end';
+
+ /**
+ * Determines if the datepicker should be a fully rounded pill shape
+ * @default false
+ */
+ pill?: boolean;
+
+ /**
+ * Extends datepicker to 100% of available width
+ * @default false
+ */
+ block?: boolean;
+
+ /**
+ * Show a clear control to unset the value
+ * @default false
+ */
+ clearable?: boolean;
+
+ /**
+ * Disable interactions and apply disabled styles
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Additional CSS class names
+ * Merged with generated classes
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ * Applied after CSS-in-JS styles
+ */
+ cssStyle?: React.CSSProperties;
+
+ /**
+ * Callback fired when date changes
+ */
+ onChange?: (date: Dayjs | null, dateString: string) => void;
+}
+
+/**
+ * Parse ISO date string into Dayjs
+ */
+const parseIsoDate = (value?: string): Dayjs | undefined => {
+ if (!value) return undefined;
+ return dayjs(value);
+};
+
+/**
+ * DatePicker Component - For selecting dates from a calendar
+ *
+ * A customizable date picker component with support for date range constraints,
+ * various visual styles, and form integration.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "DatePicker",
+ * "name": "birthday",
+ * "placeholder": "Pick a date",
+ * "variant": "outline",
+ * "size": "md",
+ * "clearable": true
+ * }
+ * ```
+ */
+export const DatePicker: React.FC = ({
+ name,
+ onChangeAction,
+ placeholder = 'Pick a date',
+ defaultValue,
+ min,
+ max,
+ variant = 'outline',
+ size = 'md',
+ side,
+ align = 'center',
+ pill = false,
+ block = false,
+ clearable = false,
+ disabled = false,
+ className = '',
+ cssStyle,
+ onChange,
+}) => {
+ const [value, setValue] = useState(defaultValue ? dayjs(defaultValue) : undefined);
+ const { executeAction } = useActionExecutor();
+
+ // Convert min/max to Dayjs objects
+ const minDate = useMemo(() => parseIsoDate(min), [min]);
+ const maxDate = useMemo(() => parseIsoDate(max), [max]);
+
+ const handleSelect = (date: Dayjs | null, dateString: string) => {
+ setValue(date ?? undefined);
+ onChange?.(date ?? undefined, dateString);
+ executeAction(onChangeAction, {
+ payload: {
+ value: dateString || null,
+ isoValue: date?.toISOString?.(),
+ fieldName: name,
+ isCleared: !dateString,
+ },
+ });
+ };
+
+ return (
+
+
+ name={name}
+ className="w-full"
+ placeholder={placeholder}
+ defaultValue={defaultValue ? dayjs(defaultValue) : undefined}
+ value={value}
+ onChange={handleSelect}
+ minDate={minDate}
+ maxDate={maxDate}
+ disabled={disabled}
+ allowClear={clearable}
+ />
+
+ );
+};
+
+export default DatePicker;
diff --git a/web/src/components/widgets/Divider.tsx b/web/src/components/widgets/Divider.tsx
new file mode 100644
index 0000000..1e0d35e
--- /dev/null
+++ b/web/src/components/widgets/Divider.tsx
@@ -0,0 +1,233 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import { isTextColorToken, isPrimitiveColorToken, isCSSColor, resolveColorValue } from './colorUtils';
+
+/**
+ * Background configuration object for theme-aware colors
+ */
+export interface ColorConfig {
+ light?: string;
+ dark?: string;
+}
+
+/**
+ * Divider component props
+ */
+export interface DividerProps {
+ /**
+ * Color of the divider; accepts border color token, a primitive color token, a CSS string, or theme-aware { light, dark }
+ * Valid tokens: "default", "subtle", "strong"
+ * Primitive color token: e.g. "red-100", "blue-900", "gray-500"
+ * @default "default"
+ */
+ color?: string | ColorConfig;
+
+ /**
+ * Thickness of the divider line; accepts a numeric pixel value or a CSS string
+ * @default 1
+ */
+ size?: string | number;
+
+ /**
+ * Outer spacing applied above and below the divider; accepts a spacing unit or a CSS string
+ * By default, the divider will space itself dynamically based on its siblings' default spacings
+ */
+ spacing?: string | number;
+
+ /**
+ * Flush the divider to the edge of its container, removing surrounding padding
+ * @default false
+ */
+ flush?: boolean;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Border color token to CSS variable mapping
+ */
+const borderColorTokens: Record = {
+ default: 'var(--color-border)',
+ subtle: 'var(--color-border-subtle, var(--color-border))',
+ strong: 'var(--color-border-strong, var(--color-border))',
+};
+
+/**
+ * Check if a color value is a border color token
+ */
+const isBorderColorToken = (color: string): boolean => {
+ return color in borderColorTokens;
+};
+
+/**
+ * Get color value from token or config
+ * Supports:
+ * 1. Border color tokens (default, subtle, strong)
+ * 2. Primitive color tokens (red-100, blue-900, etc)
+ * 3. CSS colors (hex, rgb, named colors)
+ * 4. Theme-aware config objects
+ */
+const getColorValue = (color: string | ColorConfig | undefined): string => {
+ if (!color) return borderColorTokens.default;
+
+ // Handle theme-aware config objects
+ if (typeof color === 'object') {
+ // For theme-aware config, return light mode value as fallback
+ const colorValue = color.light || color.dark || borderColorTokens.default;
+ return getColorValue(colorValue);
+ }
+
+ // Handle border color tokens
+ if (isBorderColorToken(color)) {
+ return borderColorTokens[color];
+ }
+
+ // Handle primitive color tokens (red-100, blue-900, etc)
+ if (isPrimitiveColorToken(color)) {
+ return `var(--${color})`;
+ }
+
+ // Handle CSS colors or direct color values
+ if (isCSSColor(color)) {
+ return color;
+ }
+
+ // Fallback: treat as custom CSS value
+ return color;
+};
+
+/**
+ * Convert size value to CSS
+ */
+const getSizeValue = (size: string | number | undefined): string => {
+ if (size === undefined) return '1px';
+ if (typeof size === 'number') return `${size}px`;
+ return size;
+};
+
+/**
+ * Convert spacing value to CSS
+ */
+const getSpacingValue = (spacing: string | number | undefined): string | undefined => {
+ if (spacing === undefined) return undefined;
+ if (typeof spacing === 'number') return `${spacing * 0.25}rem`;
+ return spacing;
+};
+
+/**
+ * Divider Component - Separate content with a thin line
+ *
+ * A lightweight divider component used to visually separate content sections.
+ * Supports customizable colors, thickness, spacing, and flexible color formats.
+ *
+ * Supported color formats:
+ * - Border color tokens: "default", "subtle", "strong"
+ * - Primitive color tokens: "red-100", "blue-900", "gray-500", etc.
+ * - CSS colors: "#fff", "rgb(255, 0, 0)", "blue", etc.
+ * - Theme-aware config: { light: "...", dark: "..." }
+ *
+ * @example
+ * // Basic usage with spacing
+ * ```tsx
+ *
+ * Section 1
+ *
+ * Section 2
+ *
+ * ```
+ *
+ * @example
+ * // Custom color and size
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * // Using primitive color token
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * // Using CSS color
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * // Theme-aware color configuration
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * // JSON configuration
+ * ```json
+ * {
+ * "type": "Divider",
+ * "color": "default",
+ * "size": 1,
+ * "spacing": 2
+ * }
+ * ```
+ */
+export const Divider: React.FC = ({
+ color = 'default',
+ size = 1,
+ spacing,
+ flush = false,
+ className = '',
+ cssStyle,
+}) => {
+ const colorValue = getColorValue(color);
+ const sizeValue = getSizeValue(size);
+ const spacingValue = getSpacingValue(spacing);
+
+ const wrapperClasses = cn(
+ 'w-divider',
+ {
+ 'divider-flush': flush,
+ },
+ className,
+ );
+
+ const inlineStyles: React.CSSProperties = {
+ ...cssStyle,
+ '--divider-color': colorValue,
+ '--divider-size': sizeValue,
+ // When flush is true, set margin-inline to negate parent padding
+ // and adjust width to compensate for negative margins
+ ...(flush && {
+ marginInlineStart: 'calc(-1 * var(--w-box-gutter-inline-start, 0px))',
+ marginInlineEnd: 'calc(-1 * var(--w-box-gutter-inline-end, 0px))',
+ width: 'calc(100% + var(--w-box-gutter-inline-start, 0px) + var(--w-box-gutter-inline-end, 0px))',
+ }),
+ // When spacing is set, use it for margin-block
+ ...(spacingValue && {
+ '--divider-spacing': spacingValue,
+ marginBlock: spacingValue,
+ }),
+ } as React.CSSProperties;
+
+ return (
+
+ );
+};
+
+export default Divider;
diff --git a/web/src/components/widgets/Form.md b/web/src/components/widgets/Form.md
new file mode 100644
index 0000000..9541bd4
--- /dev/null
+++ b/web/src/components/widgets/Form.md
@@ -0,0 +1,177 @@
+# Form Component
+
+## Overview
+
+The `Form` component provides a flexible layout container optimized for organizing form controls and handling form submission. Built with flexbox support for responsive layouts.
+
+## Basic Usage
+
+```tsx
+
+```
+
+## Props
+
+### Required Props
+
+| Prop | Type | Description |
+| ---------- | -------- | ------------------------------------ |
+| `children` | ReactNode | Child components to render in form |
+
+### Layout Props
+
+| Prop | Type | Default | Description |
+| --------- | ----------------------------------------------------------- | ------- | ------------------------------ |
+| `direction` | `"row" \| "col"` | `"col"` | Flex direction |
+| `align` | `"start" \| "center" \| "end" \| "baseline" \| "stretch"` | - | Cross-axis alignment |
+| `justify` | `"start" \| "center" \| "end" \| "stretch" \| "between" \| "around" \| "evenly"` | - | Main-axis distribution |
+| `wrap` | `"nowrap" \| "wrap" \| "wrap-reverse"` | - | Flex wrap behavior |
+| `flex` | `string \| number` | - | Flex grow/shrink factor |
+| `gap` | `string \| number` | - | Gap between children |
+
+### Spacing & Sizing Props
+
+| Prop | Type | Default | Description |
+| ----------- | --------------------------------- | ------- | ---------------------- |
+| `padding` | `string \| number \| PaddingConfig` | - | Inner padding |
+| `margin` | `string \| number \| MarginConfig` | - | Outer margin |
+| `height` | `string \| number` | - | Explicit height (px) |
+| `width` | `string \| number` | - | Explicit width (px) |
+| `size` | `string \| number` | - | Width & height shorthand |
+| `minHeight` | `string \| number` | - | Min height |
+| `minWidth` | `string \| number` | - | Min width |
+| `minSize` | `string \| number` | - | Min width & height |
+| `maxHeight` | `string \| number` | - | Max height |
+| `maxWidth` | `string \| number` | - | Max width |
+| `maxSize` | `string \| number` | - | Max width & height |
+
+### Styling Props
+
+| Prop | Type | Default | Description |
+| ------------- | ------------------------- | ------- | ------------------------- |
+| `background` | `string \| BackgroundConfig` | - | Background color |
+| `border` | `number \| BorderConfig` | - | Border styling |
+| `radius` | `string` | - | Border radius token |
+| `aspectRatio` | `string \| number` | - | Aspect ratio (e.g. 16/9) |
+
+### Form Props
+
+| Prop | Type | Description |
+| ----------------- | ------------- | -------------------------------- |
+| `onSubmitAction` | `FormAction` | Action dispatched on form submit |
+| `onSubmit` | `function` | Callback with FormData |
+| `className` | `string` | Additional CSS classes |
+| `cssStyle` | `CSSProperties` | Inline styles |
+
+## Examples
+
+### Vertical Form
+
+```tsx
+
+```
+
+### Horizontal Layout
+
+```tsx
+
+```
+
+### With Border and Background
+
+```tsx
+
+```
+
+### Nested Layout
+
+```tsx
+
+
+
+
+
+
+
+
+```
+
+### Responsive with Wrap
+
+```tsx
+
+
+
+
+
+```
+
+## JSON Configuration
+
+```json
+{
+ "type": "Form",
+ "direction": "col",
+ "gap": 2,
+ "padding": 4,
+ "onSubmitAction": { "type": "submitForm" },
+ "children": [...]
+}
+```
+
+## Form Submission
+
+The Form component automatically:
+- Collects form data from child form controls
+- Supports nested field names (dot notation)
+- Dispatches onSubmitAction with form data
+- Prevents default form submission
+
+## Accessing Form Data
+
+Form data is collected with dot-notation support for nested fields:
+
+```tsx
+
+
+
+
+
+// onSubmitAction payload:
+// {
+// formData: {
+// user: {
+// email: "...",
+// name: "..."
+// }
+// }
+// }
+```
+
+## Accessibility
+
+- Semantic HTML form element
+- Proper label association with inputs
+- Keyboard navigation support
+- Screen reader friendly
diff --git a/web/src/components/widgets/Form.tsx b/web/src/components/widgets/Form.tsx
new file mode 100644
index 0000000..edeedee
--- /dev/null
+++ b/web/src/components/widgets/Form.tsx
@@ -0,0 +1,432 @@
+import React, { useMemo } from 'react';
+import { cn } from '@/lib/utils';
+import { ActionConfig } from './types';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+/**
+ * Padding configuration object
+ */
+export interface PaddingConfig {
+ top?: number | string;
+ right?: number | string;
+ bottom?: number | string;
+ left?: number | string;
+}
+
+/**
+ * Border configuration object
+ */
+export interface BorderConfig {
+ width?: number;
+ color?: string;
+ style?: string;
+}
+
+/**
+ * Margin configuration object
+ */
+export interface MarginConfig {
+ top?: number | string;
+ right?: number | string;
+ bottom?: number | string;
+ left?: number | string;
+}
+
+/**
+ * Background configuration object
+ */
+export interface BackgroundConfig {
+ light?: string;
+ dark?: string;
+}
+
+/**
+ * Form component props
+ */
+export interface FormProps {
+ /**
+ * Child components to render inside the form container
+ */
+ children: React.ReactNode;
+
+ /**
+ * Action dispatched when the form is submitted
+ * @example onSubmitAction={{ type: 'submitForm', payload: { formId: 'contact' } }}
+ */
+ onSubmitAction?: ActionConfig;
+
+ /**
+ * Flex direction for laying out form children
+ * @default "col"
+ */
+ direction?: 'row' | 'col';
+
+ /**
+ * Cross-axis alignment of children
+ */
+ align?: 'start' | 'center' | 'end' | 'baseline' | 'stretch';
+
+ /**
+ * Main-axis distribution of children
+ */
+ justify?: 'start' | 'center' | 'end' | 'stretch' | 'between' | 'around' | 'evenly';
+
+ /**
+ * Wrap behavior for flex items
+ */
+ wrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
+
+ /**
+ * Flex growth/shrink factor
+ */
+ flex?: string | number;
+
+ /**
+ * Gap between direct children; accepts spacing unit or CSS string
+ */
+ gap?: string | number;
+
+ /**
+ * Inner padding; accepts spacing unit, CSS string, or padding object
+ */
+ padding?: string | number | PaddingConfig;
+
+ /**
+ * Border applied to the container
+ */
+ border?: number | BorderConfig;
+
+ /**
+ * Background color; accepts surface token, primitive color token, or theme-aware config
+ */
+ background?: string | BackgroundConfig;
+
+ /**
+ * Explicit height
+ */
+ height?: string | number;
+
+ /**
+ * Explicit width
+ */
+ width?: string | number;
+
+ /**
+ * Shorthand to set both width and height
+ */
+ size?: string | number;
+
+ /**
+ * Minimum height constraint
+ */
+ minHeight?: string | number;
+
+ /**
+ * Minimum width constraint
+ */
+ minWidth?: string | number;
+
+ /**
+ * Shorthand to set both minWidth and minHeight
+ */
+ minSize?: string | number;
+
+ /**
+ * Maximum height constraint
+ */
+ maxHeight?: string | number;
+
+ /**
+ * Maximum width constraint
+ */
+ maxWidth?: string | number;
+
+ /**
+ * Shorthand to set both maxWidth and maxHeight
+ */
+ maxSize?: string | number;
+
+ /**
+ * Aspect ratio (e.g., 16/9)
+ */
+ aspectRatio?: string | number;
+
+ /**
+ * Border radius token
+ */
+ radius?: 'sm' | 'md' | 'lg' | 'full' | 'xl' | '2xl' | '2xs' | 'xs' | '3xl' | '4xl' | '100%' | 'none';
+
+ /**
+ * Outer margin
+ */
+ margin?: string | number | MarginConfig;
+
+ /**
+ * Callback fired when form is submitted
+ */
+ onSubmit?: (formData: FormData) => void;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Helper function to convert spacing unit to CSS value
+ */
+const getSpacingValue = (value: string | number | undefined): string | undefined => {
+ if (value === undefined) return undefined;
+ if (typeof value === 'number') return `${value * 0.25}rem`;
+ return value;
+};
+
+/**
+ * Helper function to convert padding config to CSS
+ */
+const getPaddingStyles = (padding: string | number | PaddingConfig | undefined): React.CSSProperties => {
+ if (!padding) return {};
+ if (typeof padding === 'string' || typeof padding === 'number') {
+ return { padding: getSpacingValue(padding) };
+ }
+ return {
+ paddingTop: getSpacingValue(padding.top),
+ paddingRight: getSpacingValue(padding.right),
+ paddingBottom: getSpacingValue(padding.bottom),
+ paddingLeft: getSpacingValue(padding.left),
+ };
+};
+
+/**
+ * Helper function to convert border config to CSS
+ */
+const getBorderStyles = (border: number | BorderConfig | undefined): React.CSSProperties => {
+ if (!border) return {};
+ if (typeof border === 'number') {
+ return { border: `${border}px solid var(--border-color, #e5e7eb)` };
+ }
+ const width = border.width ?? 1;
+ const color = border.color ?? 'var(--border-color, #e5e7eb)';
+ const style = border.style ?? 'solid';
+ return { border: `${width}px ${style} ${color}` };
+};
+
+/**
+ * Helper function to convert margin config to CSS
+ */
+const getMarginStyles = (margin: string | number | MarginConfig | undefined): React.CSSProperties => {
+ if (!margin) return {};
+ if (typeof margin === 'string' || typeof margin === 'number') {
+ return { margin: getSpacingValue(margin) };
+ }
+ return {
+ marginTop: getSpacingValue(margin.top),
+ marginRight: getSpacingValue(margin.right),
+ marginBottom: getSpacingValue(margin.bottom),
+ marginLeft: getSpacingValue(margin.left),
+ };
+};
+
+/**
+ * Helper function to convert background to CSS
+ */
+const getBackgroundValue = (background: string | BackgroundConfig | undefined): string | undefined => {
+ if (!background) return undefined;
+ if (typeof background === 'string') return background;
+ // For theme-aware config, return light mode value (dark mode handled by CSS)
+ return background.light;
+};
+
+/**
+ * Helper function to convert radius token to CSS
+ */
+const getRadiusValue = (radius: string | undefined): string | undefined => {
+ if (!radius) return undefined;
+ const radiusMap: Record = {
+ '2xs': '0.25rem',
+ xs: '0.375rem',
+ sm: '0.5rem',
+ md: '0.625rem',
+ lg: '0.75rem',
+ xl: '1rem',
+ '2xl': '1.25rem',
+ '3xl': '1.5rem',
+ '4xl': '2rem',
+ full: '9999px',
+ '100%': '100%',
+ none: '0',
+ };
+ return radiusMap[radius] || radius;
+};
+
+/**
+ * Form Component - Layout container for form controls
+ *
+ * A flexible form container component with flexbox layout support and form submission handling.
+ * Optimized for organizing form controls with consistent spacing and styling.
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Form",
+ * "direction": "col",
+ * "gap": 2,
+ * "padding": 4,
+ * "children": [...]
+ * }
+ * ```
+ */
+export const Form: React.FC = ({
+ children,
+ onSubmitAction,
+ direction = 'col',
+ align,
+ justify,
+ wrap,
+ flex,
+ gap,
+ padding,
+ border,
+ background,
+ height,
+ width,
+ size,
+ minHeight,
+ minWidth,
+ minSize,
+ maxHeight,
+ maxWidth,
+ maxSize,
+ aspectRatio,
+ radius,
+ margin,
+ onSubmit,
+ className = '',
+ cssStyle,
+}) => {
+ const { executeAction, isLoading: isSubmitting } = useActionExecutor();
+
+ // Handle form submission
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Unified by executeAction Processing of forms, including data collection
+ executeAction(onSubmitAction, { event: e });
+ };
+
+ // Build styles
+ const flexDirectionClass = direction === 'row' ? 'flex-row' : 'flex-col';
+ const alignClass = align
+ ? {
+ start: 'items-start',
+ center: 'items-center',
+ end: 'items-end',
+ baseline: 'items-baseline',
+ stretch: 'items-stretch',
+ }[align]
+ : '';
+
+ const justifyClass = justify
+ ? {
+ start: 'justify-start',
+ center: 'justify-center',
+ end: 'justify-end',
+ stretch: 'justify-stretch',
+ between: 'justify-between',
+ around: 'justify-around',
+ evenly: 'justify-evenly',
+ }[justify]
+ : '';
+
+ const wrapClass = wrap
+ ? {
+ nowrap: 'flex-nowrap',
+ wrap: 'flex-wrap',
+ 'wrap-reverse': 'flex-wrap-reverse',
+ }[wrap]
+ : '';
+
+ const radiusClass = radius
+ ? {
+ '2xs': 'rounded-sm',
+ xs: 'rounded-xs',
+ sm: 'rounded-sm',
+ md: 'rounded',
+ lg: 'rounded-lg',
+ xl: 'rounded-xl',
+ '2xl': 'rounded-2xl',
+ '3xl': 'rounded-3xl',
+ '4xl': 'rounded-4xl',
+ full: 'rounded-full',
+ '100%': 'rounded-full',
+ none: 'rounded-none',
+ }[radius]
+ : '';
+
+ const wrapperClasses = cn('flex', flexDirectionClass, alignClass, justifyClass, wrapClass, radiusClass, className);
+
+ // Build inline styles
+ const inlineStyles: React.CSSProperties = {
+ ...getPaddingStyles(padding),
+ ...getBorderStyles(border),
+ ...getMarginStyles(margin),
+ ...cssStyle,
+ ...(gap !== undefined && { gap: getSpacingValue(gap) }),
+ ...(flex !== undefined && { flex: flex }),
+ ...(background && { backgroundColor: getBackgroundValue(background) }),
+ ...(height && { height: typeof height === 'number' ? `${height}px` : height }),
+ ...(width && { width: typeof width === 'number' ? `${width}px` : width }),
+ ...(size && {
+ width: typeof size === 'number' ? `${size}px` : size,
+ height: typeof size === 'number' ? `${size}px` : size,
+ }),
+ ...(minHeight && { minHeight: typeof minHeight === 'number' ? `${minHeight}px` : minHeight }),
+ ...(minWidth && { minWidth: typeof minWidth === 'number' ? `${minWidth}px` : minWidth }),
+ ...(minSize && {
+ minWidth: typeof minSize === 'number' ? `${minSize}px` : minSize,
+ minHeight: typeof minSize === 'number' ? `${minSize}px` : minSize,
+ }),
+ ...(maxHeight && { maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight }),
+ ...(maxWidth && { maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth }),
+ ...(maxSize && {
+ maxWidth: typeof maxSize === 'number' ? `${maxSize}px` : maxSize,
+ maxHeight: typeof maxSize === 'number' ? `${maxSize}px` : maxSize,
+ }),
+ ...(aspectRatio && { aspectRatio: typeof aspectRatio === 'number' ? `${aspectRatio}` : aspectRatio }),
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default Form;
diff --git a/web/src/components/widgets/ICON_MAPPING.md b/web/src/components/widgets/ICON_MAPPING.md
new file mode 100644
index 0000000..7cfef43
--- /dev/null
+++ b/web/src/components/widgets/ICON_MAPPING.md
@@ -0,0 +1,142 @@
+# Button Icon Mapping
+
+## Overview
+
+This document explains the icon mapping system used in the Button component. ButtonIcon names are mapped to lucide-react icon component names, allowing consistent naming while leveraging the lucide-react icon library.
+
+## How It Works
+
+1. **ButtonIcon Type** (`types/widget.ts`) - Defines available icon names
+2. **Icon Mapping** (`iconMapping.ts`) - Maps ButtonIcon names to lucide-react components
+3. **Button Component** (`Button.tsx`) - Uses the mapping to render icons dynamically
+
+## Icon Mapping Reference
+
+| ButtonIcon | Lucide-React Icon | Description |
+| ------------------- | ----------------- | ------------------------- |
+| agent | Users | Multiple users/team |
+| analytics | BarChart | Analytics/chart data |
+| atom | Atom | Atom/molecular structure |
+| batch | Grid | Grid/batch processing |
+| bolt | Zap | Lightning bolt/energy |
+| book-open | BookOpen | Open book |
+| book-closed | Book | Closed book |
+| book-clock | Clock | Clock/time |
+| bug | Bug | Bug/error |
+| calendar | Calendar | Calendar/date |
+| chart | BarChart3 | Chart/statistics |
+| check | Check | Checkmark |
+| check-circle | CheckCircle | Check with circle |
+| check-circle-filled | CheckCircle2 | Filled check circle |
+| chevron-left | ChevronLeft | Left chevron |
+| chevron-right | ChevronRight | Right chevron |
+| circle-question | HelpCircle | Question circle/help |
+| compass | Compass | Compass/navigation |
+| confetti | Sparkles | Confetti/celebration |
+| cube | Cube | 3D cube |
+| desktop | Monitor | Desktop/screen |
+| document | FileText | Document/text file |
+| dot | CircleDot | Dot/bullet point |
+| dots-horizontal | MoreHorizontal | More options (horizontal) |
+| dots-vertical | MoreVertical | More options (vertical) |
+| empty-circle | Circle | Empty circle |
+| external-link | ExternalLink | External link |
+| globe | Globe | Globe/world |
+| keys | Key | Keys/keyboard |
+| lab | Beaker | Laboratory/beaker |
+| images | Image | Image/picture |
+| info | Info | Information |
+| lifesaver | LifeBuoy | Lifesaver/help |
+| lightbulb | Lightbulb | Lightbulb/idea |
+| mail | Mail | Mail/envelope |
+| map-pin | MapPin | Location/map pin |
+| maps | Map | Map |
+| mobile | Smartphone | Mobile phone |
+| name | User | User/person |
+| notebook | Notebook | Notebook |
+| notebook-pencil | NotebookPen | Notebook with pen |
+| page-blank | File | File/blank page |
+| phone | Phone | Phone call |
+| play | Play | Play button |
+| plus | Plus | Plus/add |
+| profile | User | User profile |
+| profile-card | Card | Profile card |
+| reload | RefreshCw | Reload/refresh |
+| star | Star | Star/favorite |
+| star-filled | Star | Filled star |
+| search | Search | Search/magnifying glass |
+| sparkle | Sparkles | Sparkle/shine |
+| sparkle-double | Zap | Double sparkle/lightning |
+| square-code | CodeSquare | Code square |
+| square-image | ImageSquare | Image square |
+| square-text | TextSquare | Text square |
+| suitcase | Briefcase | Suitcase/business |
+| settings-slider | Sliders | Settings/sliders |
+| user | User | User/person |
+| wreath | Wreath | Wreath |
+| write | Pen | Pen/write |
+| write-alt | Edit | Edit |
+| write-alt2 | Pencil | Pencil |
+
+## Usage
+
+### In Button Component
+
+```tsx
+import { Button } from '@/components/widgets';
+
+// Icon-only button
+
+
+// Button with label and icon
+
+
+// Button with trailing icon
+
+```
+
+### Accessing the Mapping
+
+```tsx
+import { buttonIconMap, getLucideIconName } from '@/components/widgets/iconMapping';
+
+// Get the lucide icon name
+const lucideName = getLucideIconName('plus'); // Returns 'Plus'
+
+// Access the entire mapping
+const allMappings = buttonIconMap;
+```
+
+## Icon Sizes
+
+Icons are automatically sized based on the button's `iconSize` prop:
+
+| Icon Size | Tailwind Classes |
+| ---------------- | ---------------- |
+| 3xs, 2xs, xs, sm | w-4 h-4 |
+| md, lg | w-5 h-5 |
+| xl, 2xl, 3xl | w-6 h-6 |
+
+## Adding New Icons
+
+1. Add the icon name to `ButtonIcon` type in `types/widget.ts`
+2. Add the mapping entry in `buttonIconMap` in `iconMapping.ts`
+3. Ensure the lucide-react icon exists
+
+```typescript
+// types/widget.ts
+export type ButtonIcon = 'plus' | 'minus' | 'new-icon'; // Add here
+
+// iconMapping.ts
+export const buttonIconMap: Record = {
+ plus: 'Plus',
+ minus: 'Minus',
+ 'new-icon': 'LucideIconName', // Add mapping
+};
+```
+
+## See Also
+
+- [Lucide React Icons](https://lucide.dev/) - Full icon library documentation
+- `Button.tsx` - Button component implementation
+- `types/widget.ts` - Widget type definitions
diff --git a/web/src/components/widgets/Icon.tsx b/web/src/components/widgets/Icon.tsx
new file mode 100644
index 0000000..b0ba06e
--- /dev/null
+++ b/web/src/components/widgets/Icon.tsx
@@ -0,0 +1,184 @@
+import React, { useMemo, createElement } from 'react';
+import { cn } from '@/lib/utils';
+import * as LucideIcons from 'lucide-react';
+import { buttonIconMap } from './iconMapping';
+
+/**
+ * Color configuration object for theme-aware colors
+ */
+export interface ColorConfig {
+ light?: string;
+ dark?: string;
+}
+
+/**
+ * Icon component props
+ */
+export interface IconProps {
+ /**
+ * Name of the icon to display (required)
+ * Uses the same icon names as ButtonIcon (search, star, check, etc.)
+ * Mapped to lucide-react icons
+ */
+ name: string;
+
+ /**
+ * Icon color; accepts a text color token, primitive color token, CSS string, or theme-aware config
+ * Text color tokens: prose, primary, emphasis, secondary, tertiary, success, warning, danger
+ * @default "prose"
+ */
+ color?: string | ColorConfig;
+
+ /**
+ * Size of the icon
+ * @default "md"
+ * Options: "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl"
+ */
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Text color token to CSS variable mapping
+ */
+const colorTokens: Record = {
+ prose: 'var(--color-text-prose)',
+ primary: 'var(--color-text-primary)',
+ emphasis: 'var(--color-text-emphasis)',
+ secondary: 'var(--color-text-secondary)',
+ tertiary: 'var(--color-text-tertiary)',
+ success: 'var(--color-text-success)',
+ warning: 'var(--color-text-warning)',
+ danger: 'var(--color-text-danger)',
+};
+
+/**
+ * Get color value from token or config
+ */
+const getColorValue = (color: string | ColorConfig | undefined): string => {
+ if (!color) return colorTokens.prose;
+ if (typeof color === 'string') {
+ return colorTokens[color] || color;
+ }
+ return color.light || colorTokens.prose;
+};
+
+/**
+ * Size to CSS class mapping for lucide icons
+ */
+const sizeClasses: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl', { size: number; className: string }> = {
+ xs: { size: 12, className: 'w-3 h-3' },
+ sm: { size: 14, className: 'w-3.5 h-3.5' },
+ md: { size: 16, className: 'w-4 h-4' },
+ lg: { size: 18, className: 'w-4.5 h-4.5' },
+ xl: { size: 20, className: 'w-5 h-5' },
+ '2xl': { size: 24, className: 'w-6 h-6' },
+ '3xl': { size: 30, className: 'w-7.5 h-7.5' },
+};
+
+/**
+ * Get lucide icon component by name
+ * @param iconName - Icon name in PascalCase (e.g., 'Plus', 'CheckCircle')
+ * @returns Icon component or null if not found
+ */
+function getLucideIcon(iconName: string): React.ComponentType | null {
+ const icon = (LucideIcons as Record)[iconName];
+ return (icon as React.ComponentType) || null;
+}
+
+/**
+ * Icon Component - Visual glyphs for actions and status
+ *
+ * A flexible icon component that supports:
+ * - 70+ icons from lucide-react
+ * - Multiple size options (xs to 3xl)
+ * - 8 color tokens with customization
+ * - Theme-aware color support
+ * - Full customization via className and styles
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Icon",
+ * "name": "check",
+ * "color": "success",
+ * "size": "lg"
+ * }
+ * ```
+ */
+export const Icon: React.FC = ({ name, color = 'prose', size = 'md', className = '', cssStyle }) => {
+ const colorValue = getColorValue(color);
+ const sizeConfig = sizeClasses[size];
+
+ // Get lucide icon component
+ const lucideIcon = useMemo(() => {
+ // Try to map using buttonIconMap first
+ const mappedName = (buttonIconMap as Record)[name];
+ const iconName = mappedName || name;
+ return getLucideIcon(iconName);
+ }, [name]);
+
+ console.log('Icon', name, lucideIcon);
+
+ const wrapperClasses = cn('w-icon', sizeConfig.className, className);
+
+ const inlineStyles: React.CSSProperties = {
+ ...cssStyle,
+ color: colorValue,
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ };
+
+ // Fallback when icon is not found
+ if (!lucideIcon) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {createElement(lucideIcon, {
+ size: sizeConfig.size,
+ strokeWidth: 2,
+ })}
+
+ );
+};
+
+export default Icon;
diff --git a/web/src/components/widgets/Image.tsx b/web/src/components/widgets/Image.tsx
new file mode 100644
index 0000000..b373cc6
--- /dev/null
+++ b/web/src/components/widgets/Image.tsx
@@ -0,0 +1,344 @@
+import React, { useState } from 'react';
+import { cn } from '@/lib/utils';
+import Box from './Box';
+
+/**
+ * Image component props
+ * Supports sizing, fitting, framing, and custom styling
+ */
+export interface ImageProps {
+ /**
+ * Image URL source (required)
+ * @example src="/weather-icon.svg"
+ * @example src="https://example.com/image.png"
+ */
+ src: string;
+
+ /**
+ * Alternate text for accessibility
+ * Displayed if image fails to load, used by screen readers
+ * @default ""
+ */
+ alt?: string;
+
+ /**
+ * Draw a subtle frame around the image
+ * @default false
+ */
+ frame?: boolean;
+
+ /**
+ * How the image fits within its container
+ * @default "cover"
+ * Options: "none" | "cover" | "contain" | "fill" | "scale-down"
+ */
+ fit?: 'none' | 'cover' | 'contain' | 'fill' | 'scale-down';
+
+ /**
+ * Focal position of the image within the container
+ * @default "center"
+ * Options: "center" | "top" | "bottom" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right"
+ */
+ position?: 'center' | 'top' | 'bottom' | 'left' | 'right' | 'top left' | 'top right' | 'bottom left' | 'bottom right';
+
+ /**
+ * Flush the image to the edge of its container, removing surrounding padding
+ * @default false
+ */
+ flush?: boolean;
+
+ /**
+ * Explicit width; accepts a numeric pixel value or a CSS string
+ * @example width={200}
+ * @example width="50%"
+ */
+ width?: number | string;
+
+ /**
+ * Explicit height; accepts a numeric pixel value or a CSS string
+ * @example height={200}
+ * @example height="auto"
+ */
+ height?: number | string;
+
+ /**
+ * Shorthand to set both width and height; accepts a numeric pixel value or a CSS string
+ * @example size={240}
+ * @example size="50%"
+ */
+ size?: number | string;
+
+ /**
+ * Minimum width constraint; accepts a numeric pixel value or a CSS string
+ */
+ minWidth?: number | string;
+
+ /**
+ * Minimum height constraint; accepts a numeric pixel value or a CSS string
+ */
+ minHeight?: number | string;
+
+ /**
+ * Shorthand to set both minWidth and minHeight; accepts a numeric pixel value or a CSS string
+ */
+ minSize?: number | string;
+
+ /**
+ * Maximum width constraint; accepts a numeric pixel value or a CSS string
+ */
+ maxWidth?: number | string;
+
+ /**
+ * Maximum height constraint; accepts a numeric pixel value or a CSS string
+ */
+ maxHeight?: number | string;
+
+ /**
+ * Shorthand to set both maxWidth and maxHeight; accepts a numeric pixel value or a CSS string
+ */
+ maxSize?: number | string;
+
+ /**
+ * Aspect ratio of the box (e.g., 16/9); accepts a numeric value or a CSS string
+ */
+ aspectRatio?: number | string;
+
+ /**
+ * Border radius token or value
+ * Options: "sm" | "md" | "lg" | "full" | "xl" | "2xl" | "2xs" | "xs" | "3xl" | "4xl" | "100%" | "none"
+ */
+ radius?: 'sm' | 'md' | 'lg' | 'full' | 'xl' | '2xl' | '2xs' | 'xs' | '3xl' | '4xl' | '100%' | 'none';
+
+ /**
+ * Outer margin; accepts a spacing unit, a CSS string, or a margin object
+ * @example margin={2}
+ * @example margin="1rem"
+ * @example margin={{ top: 2, bottom: 2 }}
+ */
+ margin?:
+ | number
+ | string
+ | { top?: number | string; bottom?: number | string; left?: number | string; right?: number | string };
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Convert size value to CSS
+ */
+const getSizeValue = (value: number | string | undefined): string | undefined => {
+ if (value === undefined) return undefined;
+ if (typeof value === 'number') return `${value}px`;
+ return value;
+};
+
+/**
+ * Convert margin value to CSS
+ */
+const getMarginValue = (
+ margin:
+ | number
+ | string
+ | { top?: number | string; bottom?: number | string; left?: number | string; right?: number | string }
+ | undefined,
+): React.CSSProperties => {
+ if (!margin) return {};
+
+ if (typeof margin === 'number') {
+ const val = `${margin * 0.25}rem`;
+ return { marginTop: val, marginBottom: val, marginLeft: val, marginRight: val };
+ }
+
+ if (typeof margin === 'string') {
+ return { margin };
+ }
+
+ const result: React.CSSProperties = {};
+ if (margin.top !== undefined)
+ result.marginTop = typeof margin.top === 'number' ? `${margin.top * 0.25}rem` : margin.top;
+ if (margin.bottom !== undefined)
+ result.marginBottom = typeof margin.bottom === 'number' ? `${margin.bottom * 0.25}rem` : margin.bottom;
+ if (margin.left !== undefined)
+ result.marginLeft = typeof margin.left === 'number' ? `${margin.left * 0.25}rem` : margin.left;
+ if (margin.right !== undefined)
+ result.marginRight = typeof margin.right === 'number' ? `${margin.right * 0.25}rem` : margin.right;
+ return result;
+};
+
+/**
+ * Border radius token mapping
+ */
+const radiusTokens: Record = {
+ '2xs': '0.125rem',
+ xs: '0.25rem',
+ sm: '0.375rem',
+ md: '0.5rem',
+ lg: '0.75rem',
+ xl: '1rem',
+ '2xl': '1.5rem',
+ '3xl': '2rem',
+ '4xl': '2.5rem',
+ full: '9999px',
+ '100%': '100%',
+ none: '0',
+};
+
+/**
+ * Get border radius value
+ */
+const getRadiusValue = (radius: string | undefined): string | undefined => {
+ if (!radius) return undefined;
+ return radiusTokens[radius] || radius;
+};
+
+/**
+ * Convert object-fit position value
+ */
+const getPositionValue = (position: string | undefined): string => {
+ if (!position) return 'center';
+ return position;
+};
+
+/**
+ * Image Component - Display remote images with optional framing and object-fit
+ *
+ * A flexible image component that supports:
+ * - Multiple fitting modes (cover, contain, fill, etc.)
+ * - Focal position control
+ * - Optional frame border
+ * - Comprehensive sizing constraints
+ * - Aspect ratio control
+ * - Border radius
+ * - Margin control
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Image",
+ * "src": "https://example.com/blue-chair.png",
+ * "alt": "Blue chair",
+ * "frame": true,
+ * "size": 240,
+ * "fit": "cover",
+ * "position": "center"
+ * }
+ * ```
+ */
+export const Image: React.FC = (props) => {
+ const {
+ src,
+ alt = '',
+ frame = false,
+ fit = 'cover',
+ position = 'center',
+ flush = false,
+ width,
+ height,
+ size,
+ minWidth,
+ minHeight,
+ minSize,
+ maxWidth,
+ maxHeight,
+ maxSize,
+ aspectRatio,
+ radius,
+ margin,
+ className = '',
+ cssStyle,
+ } = props;
+ const [loaded, setLoaded] = useState(false);
+
+ // Resolve dimensions
+ let finalWidth = width;
+ let finalHeight = height;
+
+ // Use size shorthand if no explicit dimensions
+ if (size !== undefined && !width && !height) {
+ finalWidth = size;
+ finalHeight = size;
+ }
+
+ // Resolve min/max constraints
+ let finalMinWidth = minWidth;
+ let finalMinHeight = minHeight;
+ let finalMaxWidth = maxWidth;
+ let finalMaxHeight = maxHeight;
+
+ if (minSize !== undefined && !minWidth && !minHeight) {
+ finalMinWidth = minSize;
+ finalMinHeight = minSize;
+ }
+
+ if (maxSize !== undefined && !maxWidth && !maxHeight) {
+ finalMaxWidth = maxSize;
+ finalMaxHeight = maxSize;
+ }
+
+ const positionValue = getPositionValue(position);
+ const radiusValue = getRadiusValue(radius);
+ const marginStyles = getMarginValue(margin);
+
+ // Determine if dimensions are set
+ const hasWidth = width !== undefined || size !== undefined;
+ const hasHeight = height !== undefined || size !== undefined;
+
+ const wrapperClasses = cn(
+ {
+ 'image-frame': frame,
+ 'image-flush': flush,
+ 'image-loaded': loaded,
+ },
+ className,
+ );
+
+ const imageStyle: React.CSSProperties = {
+ display: 'block',
+ width: getSizeValue(finalWidth as number | string) || '100%',
+ height: getSizeValue(finalHeight as number | string),
+ minWidth: getSizeValue(finalMinWidth as number | string),
+ minHeight: getSizeValue(finalMinHeight as number | string),
+ maxWidth: getSizeValue(finalMaxWidth as number | string),
+ maxHeight: getSizeValue(finalMaxHeight as number | string),
+ aspectRatio: typeof aspectRatio === 'number' ? aspectRatio : aspectRatio,
+ objectFit: fit,
+ objectPosition: positionValue,
+ borderRadius: radiusValue,
+ ...marginStyles,
+ ...cssStyle,
+ };
+
+ return (
+
+ setLoaded(true)} draggable={false} data-loaded="" />
+
+ );
+};
+
+export default Image;
diff --git a/web/src/components/widgets/Input.tsx b/web/src/components/widgets/Input.tsx
new file mode 100644
index 0000000..9080b92
--- /dev/null
+++ b/web/src/components/widgets/Input.tsx
@@ -0,0 +1,239 @@
+import React, { useRef, useEffect, useState } from 'react';
+
+import './input.css';
+
+/**
+ * Input action object
+ */
+export interface InputAction {
+ type: string;
+ payload?: unknown;
+}
+
+/**
+ * Input component props
+ */
+export interface InputProps {
+ /**
+ * The name of the form control field (required)
+ * When the form is submitted, the value will be included in onSubmitAction payload
+ * Supports dot-separated paths: "myData.myFieldName" → payload.myData.myFieldName
+ */
+ name: string;
+
+ /**
+ * Native input type
+ * @default "text"
+ */
+ inputType?: 'text' | 'number' | 'email' | 'password' | 'tel' | 'url';
+
+ /**
+ * Initial value of the input
+ */
+ defaultValue?: string;
+
+ /**
+ * Visual style of the input
+ * @default "outline"
+ */
+ variant?: 'soft' | 'outline';
+
+ /**
+ * Controls the size of the input
+ * @default "md"
+ * Size to height mapping:
+ * | Size | Height |
+ * |------|--------|
+ * | 3xs | 22px |
+ * | 2xs | 24px |
+ * | xs | 26px |
+ * | sm | 28px |
+ * | md | 32px |
+ * | lg | 36px |
+ * | xl | 40px |
+ * | 2xl | 44px |
+ * | 3xl | 48px |
+ */
+ size?: '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
+
+ /**
+ * Controls gutter on the edges of the input
+ * Defaults to value from size
+ * | Size | Gutter |
+ * |------|--------|
+ * | 2xs | 6px |
+ * | xs | 8px |
+ * | sm | 10px |
+ * | md | 12px |
+ * | lg | 14px |
+ * | xl | 16px |
+ */
+ gutterSize?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+ /**
+ * Mark the input as required for form submission
+ * @default false
+ */
+ required?: boolean;
+
+ /**
+ * Regex pattern for input validation
+ */
+ pattern?: string;
+
+ /**
+ * Placeholder text shown when empty
+ */
+ placeholder?: string;
+
+ /**
+ * Allow password managers / autofill extensions to appear
+ * Defaults to true for password type
+ */
+ allowAutofillExtensions?: boolean;
+
+ /**
+ * Select all contents of the input when it mounts
+ * @default false
+ */
+ autoSelect?: boolean;
+
+ /**
+ * Autofocus the input when it mounts
+ * @default false
+ */
+ autoFocus?: boolean;
+
+ /**
+ * Disable interactions and apply disabled styles
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Determines if the input should be a fully rounded pill shape
+ * @default false
+ */
+ pill?: boolean;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Input Component - For single-line text input
+ *
+ * A customizable text input component supporting various types, sizes, and styles.
+ * Integrated with form submission and supports validation patterns.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Input",
+ * "name": "email",
+ * "inputType": "email",
+ * "placeholder": "Enter email",
+ * "size": "md"
+ * }
+ * ```
+ */
+export const Input: React.FC = ({
+ name,
+ inputType = 'text',
+ defaultValue = '',
+ variant = 'outline',
+ size = 'md',
+ gutterSize,
+ required = false,
+ pattern,
+ placeholder,
+ allowAutofillExtensions,
+ autoSelect = false,
+ autoFocus = false,
+ disabled = false,
+ pill = false,
+ className = '',
+ cssStyle,
+}) => {
+ const inputRef = useRef(null);
+
+ const [focused, setFocused] = useState(false);
+
+ // Handle auto-select on mount
+ useEffect(() => {
+ if (autoSelect && inputRef.current) {
+ inputRef.current.select();
+ }
+ }, [autoSelect]);
+
+ // Determine if autofill should be allowed
+ const shouldAllowAutofill =
+ allowAutofillExtensions !== undefined ? allowAutofillExtensions : inputType === 'password';
+
+ // Handle blur
+ const handleBlur = (e: React.FocusEvent) => {
+ setFocused(false);
+ };
+
+ // Handle focus
+ const handleFocus = (e: React.FocusEvent) => {
+ setFocused(true);
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default Input;
diff --git a/web/src/components/widgets/Label.tsx b/web/src/components/widgets/Label.tsx
new file mode 100644
index 0000000..e8c957f
--- /dev/null
+++ b/web/src/components/widgets/Label.tsx
@@ -0,0 +1,185 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+/**
+ * Color configuration object for theme-aware colors
+ */
+export interface ColorConfig {
+ light?: string;
+ dark?: string;
+}
+
+/**
+ * Label component props
+ */
+export interface LabelProps {
+ /**
+ * Text content of the label (required)
+ */
+ value: string;
+
+ /**
+ * Name of the field this label describes (required)
+ * Used for the 'htmlFor' attribute to associate with form control
+ */
+ fieldName: string;
+
+ /**
+ * Size of the label text
+ * @default "sm"
+ */
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+ /**
+ * Font weight
+ * @default "medium"
+ */
+ weight?: 'normal' | 'medium' | 'semibold' | 'bold';
+
+ /**
+ * Horizontal text alignment
+ * @default "start"
+ */
+ textAlign?: 'start' | 'center' | 'end';
+
+ /**
+ * Text color
+ * Accepts text color token, primitive color token, CSS string, or theme-aware config
+ * Text color tokens: prose, primary, emphasis, secondary, tertiary, success, warning, danger
+ * @default "secondary"
+ */
+ color?: string | ColorConfig;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Text color token to CSS variable mapping
+ */
+const colorTokens: Record = {
+ prose: 'var(--color-text-prose)',
+ primary: 'var(--color-text-primary)',
+ emphasis: 'var(--color-text-emphasis)',
+ secondary: 'var(--color-text-secondary)',
+ tertiary: 'var(--color-text-tertiary)',
+ success: 'var(--color-text-success)',
+ warning: 'var(--color-text-warning)',
+ danger: 'var(--color-text-danger)',
+};
+
+/**
+ * Get color value from token or config
+ */
+const getColorValue = (color: string | ColorConfig | undefined): string => {
+ if (!color) return colorTokens.secondary;
+ if (typeof color === 'string') {
+ return colorTokens[color] || color;
+ }
+ // For theme-aware config, return light mode value
+ return color.light || colorTokens.secondary;
+};
+
+/**
+ * Size to CSS class mapping
+ */
+const sizeClasses: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', string> = {
+ xs: 'text-xs',
+ sm: 'text-sm',
+ md: 'text-base',
+ lg: 'text-lg',
+ xl: 'text-xl',
+};
+
+/**
+ * Weight to CSS class mapping
+ */
+const weightClasses: Record<'normal' | 'medium' | 'semibold' | 'bold', string> = {
+ normal: 'font-normal',
+ medium: 'font-medium',
+ semibold: 'font-semibold',
+ bold: 'font-bold',
+};
+
+/**
+ * Text alignment to CSS class mapping
+ */
+const textAlignClasses: Record<'start' | 'center' | 'end', string> = {
+ start: 'text-left',
+ center: 'text-center',
+ end: 'text-right',
+};
+
+/**
+ * Label Component - Accessible label for a form field
+ *
+ * A semantic label component that properly associates with form controls
+ * using the htmlFor attribute for accessibility.
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Label",
+ * "value": "Email",
+ * "fieldName": "email",
+ * "size": "sm",
+ * "weight": "medium"
+ * }
+ * ```
+ */
+export const Label: React.FC = ({
+ value,
+ fieldName,
+ size = 'sm',
+ weight = 'medium',
+ textAlign = 'start',
+ color = 'secondary',
+ className = '',
+ cssStyle,
+}) => {
+ const colorValue = getColorValue(color);
+ const sizeClass = sizeClasses[size];
+ const weightClass = weightClasses[weight];
+ const alignClass = textAlignClasses[textAlign];
+
+ const wrapperClasses = cn('w-label', 'inline-block', sizeClass, weightClass, alignClass, className);
+
+ const inlineStyles: React.CSSProperties = {
+ ...cssStyle,
+ color: colorValue,
+ };
+
+ return (
+
+ {value}
+
+ );
+};
+
+export default Label;
diff --git a/web/src/components/widgets/ListView.tsx b/web/src/components/widgets/ListView.tsx
new file mode 100644
index 0000000..5e58860
--- /dev/null
+++ b/web/src/components/widgets/ListView.tsx
@@ -0,0 +1,39 @@
+import React, { PropsWithChildren, useMemo } from 'react';
+import Icon from './Icon';
+import './listview.css';
+import Row from './Row';
+
+interface ListViewProps {
+ limit?: number;
+ status?: {
+ text: string;
+ icon: string;
+ };
+ theme?: 'light' | 'dark' | undefined;
+ className?: string;
+}
+
+const ListView: React.FC> = (props) => {
+ const { limit = 4, status, theme, children, className } = props;
+ console.log('ListView', props);
+ return (
+
+ {(status?.text || status?.icon) && (
+
+ {status.icon && }
+ {status?.text && {status.text}
}
+
+ )}
+ {children}
+
+ );
+};
+
+export default ListView;
diff --git a/web/src/components/widgets/ListViewItem.tsx b/web/src/components/widgets/ListViewItem.tsx
new file mode 100644
index 0000000..c976033
--- /dev/null
+++ b/web/src/components/widgets/ListViewItem.tsx
@@ -0,0 +1,59 @@
+import React, { PropsWithChildren, useMemo } from 'react';
+import Row from './Row';
+import { ActionConfig } from './types';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+interface ListViewItemProps {
+ gap?: number | string;
+
+ align?: 'start' | 'center' | 'end' | 'stretch';
+
+ onClickAction?: ActionConfig;
+}
+
+type InteractiveProps = React.HTMLAttributes & {
+ 'data-w-clickable'?: string;
+};
+
+const ListViewItem: React.FC> = ({ onClickAction, ...props }) => {
+ const { executeAction, isLoading } = useActionExecutor();
+
+ const triggerAction = (event?: React.SyntheticEvent) => {
+ if (!onClickAction) return;
+ event?.preventDefault();
+ event?.stopPropagation();
+ event?.persist?.();
+ executeAction(onClickAction, { event });
+ };
+
+ const handleClick: React.MouseEventHandler = (event) => {
+ triggerAction(event);
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ triggerAction(event);
+ }
+ };
+
+ const interactiveProps: InteractiveProps | undefined = onClickAction
+ ? {
+ role: 'button',
+ tabIndex: 0,
+ onClick: handleClick,
+ onKeyDown: handleKeyDown,
+ 'data-w-clickable': '',
+ 'aria-busy': isLoading ? true : undefined,
+ }
+ : undefined;
+
+ return (
+
+
+ {props.children}
+
+
+ );
+};
+
+export default ListViewItem;
diff --git a/web/src/components/widgets/Markdown.tsx b/web/src/components/widgets/Markdown.tsx
new file mode 100644
index 0000000..19b1882
--- /dev/null
+++ b/web/src/components/widgets/Markdown.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+import MarkdownInfra from '@/components/Infra/Markdown';
+
+/**
+ * Markdown widget component props
+ */
+export interface MarkdownProps {
+ /**
+ * Markdown source string to render (required)
+ */
+ value: string;
+
+ /**
+ * Applies streaming-friendly transitions for incremental updates
+ * Important: Once incremental updates are complete, this should be set to false
+ * @default false
+ */
+ streaming?: boolean;
+
+ /**
+ * Callback when code is copied from code blocks
+ */
+ onCopied?: (code: string) => void;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Markdown Component - Render rich formatted content from markdown
+ *
+ * A powerful markdown renderer that supports:
+ * - GitHub Flavored Markdown (GFM)
+ * - Math formulas (KaTeX)
+ * - Code syntax highlighting
+ * - Images and links
+ * - Emoji rendering
+ * - HTML content (with sanitization)
+ * - Streaming updates
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ * console.log('Copied:', code)}
+ * />
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Markdown",
+ * "value": "# Heading\n\nParagraph with **bold** text.",
+ * "streaming": false
+ * }
+ * ```
+ */
+export const Markdown: React.FC = ({ value, streaming = false, onCopied, className = '', cssStyle }) => {
+ const wrapperClasses = cn(
+ 'w-markdown',
+ {
+ 'markdown-streaming': streaming,
+ },
+ className,
+ );
+
+ return (
+
+
+
+ );
+};
+
+export default Markdown;
diff --git a/web/src/components/widgets/RadioGroup.tsx b/web/src/components/widgets/RadioGroup.tsx
new file mode 100644
index 0000000..bdca4e1
--- /dev/null
+++ b/web/src/components/widgets/RadioGroup.tsx
@@ -0,0 +1,158 @@
+import React, { useMemo, useState } from 'react';
+import { Radio as AntdRadio, RadioChangeEvent } from 'antd';
+import { ActionConfig } from './types';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+const AntdRadioGroup = AntdRadio.Group;
+
+/**
+ * Radio option object
+ */
+export interface RadioOption {
+ label: string;
+ value: string;
+ disabled?: boolean;
+}
+
+/**
+ * RadioGroup component props
+ */
+export interface RadioGroupProps {
+ /**
+ * The name of the form control field (required)
+ * When the form is submitted, the value will be included in onSubmitAction payload
+ * Supports dot-separated paths: "myData.myFieldName" → payload.myData.myFieldName
+ */
+ name: string;
+
+ /**
+ * Array of options to render as radio items (required)
+ * Each option has label, value, and optional disabled flag
+ * @example options={[{ label: "Small", value: "sm" }, { label: "Large", value: "lg" }]}
+ */
+ options: RadioOption[];
+
+ /**
+ * Accessible label for the radio group; falls back to name
+ * @example ariaLabel="Select a size"
+ */
+ ariaLabel?: string;
+
+ /**
+ * Action dispatched when the selected value changes
+ * @example onChangeAction={{ type: 'sizeSelected', payload: { category: 'size' } }}
+ */
+ onChangeAction?: ActionConfig;
+
+ /**
+ * Initial selected value of the radio group
+ * @example defaultValue="md"
+ */
+ defaultValue?: string;
+
+ /**
+ * Layout direction of the radio items
+ * @default "row"
+ */
+ direction?: 'row' | 'col';
+
+ /**
+ * Disable interactions and apply disabled styles for the entire group
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Mark the group as required for form submission
+ * @default false
+ */
+ required?: boolean;
+}
+
+/**
+ * RadioGroup Component - For single option selection
+ *
+ * A customizable radio group component for selecting a single option from multiple choices.
+ * Integrated with form submission and supports various layout directions.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "RadioGroup",
+ * "name": "size",
+ * "options": [
+ * { "label": "Small", "value": "sm" },
+ * { "label": "Large", "value": "lg" }
+ * ],
+ * "direction": "row"
+ * }
+ * ```
+ */
+export const RadioGroup: React.FC = ({
+ name,
+ options,
+ ariaLabel,
+ onChangeAction,
+ defaultValue = '',
+ direction = 'row',
+ disabled = false,
+ required = false,
+}) => {
+ const [value, setValue] = useState(defaultValue);
+ const { executeAction } = useActionExecutor();
+
+ // Handle value change
+ const handleValueChange = (e: RadioChangeEvent) => {
+ const newValue = e.target.value;
+ setValue(newValue);
+
+ executeAction(onChangeAction, {
+ payload: {
+ value: newValue,
+ fieldName: name,
+ },
+ event: e.nativeEvent,
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default RadioGroup;
diff --git a/web/src/components/widgets/Row.tsx b/web/src/components/widgets/Row.tsx
new file mode 100644
index 0000000..039f2ed
--- /dev/null
+++ b/web/src/components/widgets/Row.tsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import Box from './Box';
+import { StyleableProps } from './types';
+
+/**
+ * Row component props - extends StyleableProps with horizontal layout preset
+ */
+export type RowProps = StyleableProps;
+
+/**
+ * Row Component - Horizontal flex layout container
+ *
+ * A convenience component that renders a Box with horizontal (row) direction.
+ * Perfect for creating horizontal layouts, navigation bars, toolbars, or inline content arrangements.
+ *
+ * Creates a flexbox container with flex-direction set to row, allowing you to
+ * arrange children horizontally with consistent spacing and alignment control.
+ *
+ * Layout Properties:
+ * - gap: Space between direct children (rem units or CSS string)
+ * - padding: Inner padding (rem units, CSS string, or padding object)
+ * - align: Cross-axis alignment - 'start', 'center', 'end', 'stretch'
+ * - justify: Main-axis distribution - 'start', 'center', 'end', 'between', 'around'
+ * - wrap: Flex wrap behavior - 'nowrap', 'wrap', 'wrap-reverse'
+ *
+ * Size Properties:
+ * - width: Explicit width (pixels or CSS string)
+ * - height: Explicit height (pixels or CSS string)
+ * - minWidth, maxWidth: Width constraints (pixels)
+ * - minHeight, maxHeight: Height constraints (pixels)
+ *
+ * Style Properties:
+ * - background: Background color token or CSS color
+ * - radius: Border radius preset - 'xs', 'sm', 'md', 'lg', 'xl', 'full'
+ * - flex: Flex growth/shrink factor
+ *
+ * HTML Properties:
+ * - children: Child components to render
+ * - className: Additional CSS class names
+ * - style: Inline CSS styles
+ *
+ * @example
+ * Horizontal layout with three items centered:
+ *
+ *
+ *
+ *
+ *
+ *
+ * @example
+ * Row with space-between distribution and background:
+ *
+ * {children}
+ *
+ *
+ * @example
+ * Navigation bar with items spaced apart:
+ *
+ * {navItems}
+ *
+ *
+ * @example
+ * JSON configuration for horizontal layout:
+ * {
+ * "type": "Row",
+ * "gap": 2,
+ * "align": "center",
+ * "justify": "between",
+ * "children": [
+ * { "type": "Text", "value": "One" },
+ * { "type": "Text", "value": "Two" },
+ * { "type": "Text", "value": "Three" }
+ * ]
+ * }
+ */
+export const Row: React.FC = (props) => {
+ return ;
+};
+
+export default Row;
diff --git a/web/src/components/widgets/Select.css b/web/src/components/widgets/Select.css
new file mode 100644
index 0000000..70130fa
--- /dev/null
+++ b/web/src/components/widgets/Select.css
@@ -0,0 +1,39 @@
+[data-w-component='select'] {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--select-control-gap);
+ flex-shrink: 0;
+ height: var(--select-control-size);
+ border-radius: var(--select-control-radius);
+ cursor: pointer;
+ font-size: var(--select-control-font-size);
+ font-weight: var(--select-control-font-weight);
+ line-height: var(--font-text-md-line-height);
+ text-align: left;
+ transition-duration: var(--transition-duration-basic);
+ transition-property: color;
+ transition-timing-function: var(--transition-ease-basic);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+ box-shadow: 0 0 0 1px var(--select-control-border-color) inset;
+ background-color: var(--select-control-background-color);
+}
+
+[data-w-component='select'] .ant-select-single:not(.ant-select-customize-input) .ant-select-selector {
+ padding: 0 var(--select-control-gutter);
+}
+
+[data-w-component='select'][data-variant='outline'] {
+ --select-border-focus-color: var(--input-outline-border-color-focus);
+ --select-control-border-color: var(--input-outline-border-color);
+ --select-control-ring-offset: -1px;
+}
+
+/* ghost variant */
+[data-w-component='select'][data-variant='ghost']:hover {
+ background-color: var(--select-control-background-color);
+}
diff --git a/web/src/components/widgets/Select.tsx b/web/src/components/widgets/Select.tsx
new file mode 100644
index 0000000..9826990
--- /dev/null
+++ b/web/src/components/widgets/Select.tsx
@@ -0,0 +1,201 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import type { ControlVariant, ControlSize } from '@/types';
+import { Select as AntdSelect } from 'antd';
+import './Select.css';
+import { ActionConfig } from './types';
+import { useActionExecutor } from '@/hooks/useActionExecutor';
+
+/**
+ * Select option object
+ */
+export interface SelectOption {
+ label: string;
+ value: string;
+ disabled?: boolean;
+}
+
+/**
+ * Select component props
+ */
+export interface SelectProps {
+ /**
+ * The name of the form control field (required)
+ * When the form is submitted, the value will be included in onSubmitAction payload
+ * Supports dot-separated paths: "myData.myFieldName" → payload.myData.myFieldName
+ */
+ name: string;
+
+ /**
+ * List of selectable options (required)
+ * Each option has label, value, and optional disabled flag
+ * @example options={[{ label: "Option A", value: "a" }, { label: "Option B", value: "b" }]}
+ */
+ options: SelectOption[];
+
+ /**
+ * Action dispatched when the value changes
+ * @example onChangeAction={{ type: 'selectionChanged', payload: { field: 'choice' } }}
+ */
+ onChangeAction?: ActionConfig;
+
+ /**
+ * Placeholder text shown when no value is selected
+ * @example placeholder="Choose an option"
+ */
+ placeholder?: string;
+
+ /**
+ * Initial value of the select
+ * @example defaultValue="option-a"
+ */
+ defaultValue?: string;
+
+ /**
+ * Visual variant of the select control
+ * @default "outline"
+ */
+ variant?: ControlVariant;
+
+ /**
+ * Controls the size of the select
+ * @default "md"
+ */
+ size?: ControlSize;
+
+ /**
+ * Determines if the select should be a fully rounded pill shape
+ * @default false
+ */
+ pill?: boolean;
+
+ /**
+ * Extends select to 100% of available width
+ * @default false
+ */
+ block?: boolean;
+
+ /**
+ * Show a clear control to unset the value
+ * @default false
+ */
+ clearable?: boolean;
+
+ /**
+ * Disable interactions and apply disabled styles
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Callback fired when value changes
+ */
+ onChange?: (value: string) => void;
+}
+
+/**
+ * Select Component - For selecting from a list of options
+ *
+ * A customizable dropdown select component with support for custom options,
+ * various visual styles, and form integration.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Select",
+ * "name": "choice",
+ * "options": [{ "label": "A", "value": "a" }],
+ * "placeholder": "Choose an option",
+ * "variant": "outline",
+ * "size": "md"
+ * }
+ * ```
+ */
+export const Select: React.FC = ({
+ name,
+ options,
+ onChangeAction,
+ placeholder = 'Select...',
+ defaultValue,
+ variant = 'outline',
+ size = 'md',
+ pill = false,
+ block = false,
+ clearable = false,
+ disabled = false,
+
+ onChange,
+}) => {
+ const [value, setValue] = useState(defaultValue);
+ const { executeAction } = useActionExecutor();
+
+ useEffect(() => {
+ setValue(defaultValue);
+ }, [defaultValue]);
+
+ // Handle value change
+ const handleValueChange = (newValue: string | undefined) => {
+ setValue(newValue);
+
+ if (onChange) {
+ onChange(newValue ?? '');
+ }
+
+ executeAction(onChangeAction, {
+ payload: {
+ value: newValue,
+ fieldName: name,
+ },
+ });
+ };
+
+ // Find selected option for display
+ // const selectedOption = useMemo(() => options.find((opt) => opt.value === value), [value, options]);
+ return (
+
+
+
handleValueChange(newValue as string | undefined)}
+ disabled={disabled}
+ options={options}
+ className="w-full"
+ placeholder={placeholder}
+ variant="borderless"
+ allowClear={clearable}
+ />
+
+ );
+};
+
+export default Select;
diff --git a/web/src/components/widgets/Spacer.tsx b/web/src/components/widgets/Spacer.tsx
new file mode 100644
index 0000000..1cab27a
--- /dev/null
+++ b/web/src/components/widgets/Spacer.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { cn } from '@/lib/utils';
+
+/**
+ * Spacer component props
+ */
+export interface SpacerProps {
+ /**
+ * Minimum size the spacer should occupy along the flex direction
+ * Accepts a numeric pixel value or a CSS string
+ * @default "auto"
+ * @example minSize={16} // 16px
+ * @example minSize="1rem" // 1rem
+ */
+ minSize?: string | number;
+
+ /**
+ * Additional CSS class names
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ */
+ cssStyle?: React.CSSProperties;
+}
+
+/**
+ * Spacer Component - Flexible space to separate content
+ *
+ * A lightweight spacer component that expands to fill available space in flex containers.
+ * Useful for pushing content apart or creating flexible spacing in layouts.
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Spacer",
+ * "minSize": 16
+ * }
+ * ```
+ */
+export const Spacer: React.FC = ({ minSize = 'auto', className = '', cssStyle }) => {
+ // Convert minSize to CSS value
+ const getMinSizeValue = (size: string | number | undefined): string => {
+ if (size === undefined || size === 'auto') return 'auto';
+ if (typeof size === 'number') return `${size}px`;
+ return size;
+ };
+
+ const minSizeValue = getMinSizeValue(minSize);
+
+ const wrapperClasses = cn('flex-1', 'w-spacer', className);
+
+ const inlineStyles: React.CSSProperties = {
+ ...cssStyle,
+ flex: '1 1 auto',
+ minWidth: minSizeValue,
+ minHeight: minSizeValue,
+ '--w-spacer-min-size': minSizeValue,
+ } as React.CSSProperties;
+
+ return
;
+};
+
+export default Spacer;
diff --git a/web/src/components/widgets/Text.tsx b/web/src/components/widgets/Text.tsx
new file mode 100644
index 0000000..16d80d4
--- /dev/null
+++ b/web/src/components/widgets/Text.tsx
@@ -0,0 +1,266 @@
+import React from 'react';
+import { TextWeight, TextSize, TextColor } from '@/types';
+import useStyleBuilder from './styleBuilder';
+import { cn } from '@/lib/utils';
+import { resolveColorValue } from './colorUtils';
+import { Input } from 'antd';
+
+/**
+ * Text component props
+ * Supports typography styling with weight, size, and color
+ */
+export interface TextProps {
+ /**
+ * Text content to display
+ * Use either value or children, value takes precedence if both provided
+ * @example value="Hello World"
+ */
+ value?: string;
+
+ /**
+ * Child content to render
+ * Used if value is not provided
+ * @example children={Bold text }
+ */
+ children?: React.ReactNode;
+
+ /**
+ * Font weight of the text
+ * "normal" "medium" "semibold" "bold"
+ * @default "normal"
+ */
+ weight?: TextWeight;
+
+ /**
+ * Font size of the text
+ * Predefined size scale from xs to xl
+ * "sm" "md" "lg" "xl" "xs"
+ * @default "md"
+ */
+ size?: TextSize;
+
+ /**
+ * Text color; supports three formats:
+ * 1. Text color tokens: prose, primary, emphasis, secondary, tertiary, success, warning, danger
+ * 2. Primitive color token: e.g. red-100, blue-900, gray-500
+ * 3. CSS color: e.g. blue, #112233, rgb(1,2,3)
+ */
+ color?: TextColor | string;
+
+ /**
+ * Enables streaming-friendly transitions for incremental updates
+ * Important: Once incremental updates are complete, this should be set to false
+ * Note: Streaming animations are not currently implemented for basic text components,
+ * but we plan to add them in the future.
+ * @default false
+ */
+ streaming?: boolean;
+
+ /**
+ * Render text in italic style
+ * @example italic={true}
+ */
+ italic?: boolean;
+
+ /**
+ * Render text with a line-through decoration
+ * @example lineThrough={true}
+ */
+ lineThrough?: boolean;
+
+ /**
+ * Constrain the text container width
+ * Accepts a numeric pixel value or a CSS string
+ * @example width={200} // 200px
+ * @example width="100%" // full width
+ */
+ width?: string | number;
+
+ /**
+ * Forces the text container to reserve space for a minimum number of lines
+ * @example minLines={3}
+ */
+ minLines?: number;
+
+ /**
+ * Horizontal text alignment
+ * @default "start"
+ * @example textAlign="center"
+ */
+ textAlign?: 'start' | 'center' | 'end';
+
+ /**
+ * Truncate overflow with ellipsis
+ * Applies single-line truncation
+ * @default false
+ * @example truncate={true}
+ */
+ truncate?: boolean;
+
+ /**
+ * Limit text to a maximum number of lines
+ * Applies a line clamp
+ * @example maxLines={2}
+ */
+ maxLines?: number;
+
+ /**
+ * Additional CSS class names
+ * Merged with generated classes
+ * @example className="line-through" // strikethrough text
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ * Applied after CSS-in-JS styles
+ * @example style={{ letterSpacing: "0.1em" }}
+ */
+ style?: React.CSSProperties;
+
+ /**
+ * Enable inline editing for this text node.
+ *
+ * @default false
+ */
+ editable?:
+ | false
+ | {
+ /**
+ * The name of the form control field.
+ * When the form is submitted, the value of this field will be included in the form's \`onSubmitAction\` payload.
+ *
+ * **Note:** Dot-separated paths are supported. e.g. \`"myData.myFieldName"\` → \`payload.myData.myFieldName\`
+ */
+ name: string;
+ /** Placeholder text for the editable input. */
+ placeholder?: string;
+ /**
+ * Autofocus the editable input when it appears.
+ * @default false
+ */
+ autoFocus?: boolean;
+ /**
+ * Select all text on focus.
+ * @default false
+ */
+ autoSelect?: boolean;
+ /** Native autocomplete hint for the input. */
+ autoComplete?: string;
+ /**
+ * Allow browser password/autofill extensions.
+ * @default false
+ */
+ allowAutofillExtensions?: boolean;
+ /** Regex pattern for input validation. */
+ pattern?: string;
+ /**
+ * Mark the editable input as required.
+ * @default false
+ */
+ required?: boolean;
+ };
+}
+
+/**
+ * Text Component - For rendering text content
+ *
+ * A typography component that renders spans with flexible styling options.
+ * Supports font weight, size, and color customization.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Text",
+ * "value": "Hello",
+ * "weight": "semibold",
+ * "size": "lg",
+ * "color": "primary"
+ * }
+ * ```
+ */
+export const Text: React.FC = ({
+ value,
+ children,
+ size = 'md',
+ color = 'prose',
+ weight = 'normal',
+ className = '',
+ italic = false,
+ lineThrough = false,
+ textAlign = 'start',
+ truncate = false,
+ maxLines = undefined,
+ editable,
+ minLines = 1,
+ ...props
+}) => {
+ // Resolve color value (text token, primitive token, or CSS color)
+ const { style: colorStyle, className: colorClassName } = resolveColorValue(color);
+
+ // Build font size, line height, and font weight CSS variables
+ const fontSizeValue = size ? `var(--font-text-${size}-size)` : undefined;
+ const lineHeightValue = size ? `var(--font-text-${size}-line-height)` : undefined;
+ const fontWeightValue = weight ? `var(--font-weight-${weight})` : undefined;
+
+ const defaultClassName = cn(['w-text'], colorClassName, className);
+
+ const styles: React.CSSProperties = {
+ ...(fontSizeValue && { fontSize: fontSizeValue }),
+ ...(lineHeightValue && { lineHeight: lineHeightValue }),
+ ...(fontWeightValue && { fontWeight: fontWeightValue }),
+ ...(colorStyle && { color: colorStyle }),
+ ...(italic && { fontStyle: 'italic' }),
+ ...(lineThrough && { textDecoration: 'line-through' }),
+ ...(textAlign && { textAlign: textAlign }),
+ ...(truncate && {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ }),
+ ...(maxLines && {
+ display: '-webkit-box',
+ WebkitLineClamp: maxLines,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ }),
+ };
+
+ const { style: textStyle, className: textClassName } = useStyleBuilder({
+ ...props,
+ className: defaultClassName,
+ style: styles,
+ });
+
+ if (editable) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {value !== undefined ? value : children}
+
+ );
+};
+
+export default Text;
diff --git a/web/src/components/widgets/Textarea.tsx b/web/src/components/widgets/Textarea.tsx
new file mode 100644
index 0000000..47259f1
--- /dev/null
+++ b/web/src/components/widgets/Textarea.tsx
@@ -0,0 +1,216 @@
+import React, { useRef, useEffect, useState } from 'react';
+import { Input } from 'antd';
+import type { TextAreaRef } from 'antd/es/input/TextArea';
+
+/**
+ * Textarea action object
+ */
+export interface TextareaAction {
+ type: string;
+ payload?: unknown;
+}
+
+/**
+ * Textarea component props
+ */
+export interface TextareaProps {
+ /**
+ * The name of the form control field (required)
+ * When the form is submitted, the value will be included in onSubmitAction payload
+ * Supports dot-separated paths: "myData.myFieldName" → payload.myData.myFieldName
+ */
+ name: string;
+
+ /**
+ * Initial value of the textarea
+ */
+ defaultValue?: string;
+
+ /**
+ * Mark the textarea as required for form submission
+ * @default false
+ */
+ required?: boolean;
+
+ /**
+ * Placeholder text shown when empty
+ */
+ placeholder?: string;
+
+ /**
+ * Select all contents of the textarea when it mounts
+ * @default false
+ */
+ autoSelect?: boolean;
+
+ /**
+ * Autofocus the textarea when it mounts
+ * @default false
+ */
+ autoFocus?: boolean;
+
+ /**
+ * Disable interactions and apply disabled styles
+ * @default false
+ */
+ disabled?: boolean;
+
+ /**
+ * Visual style of the textarea
+ * @default "outline"
+ */
+ variant?: 'soft' | 'outline';
+
+ /**
+ * Controls the size of the textarea
+ * @default "md"
+ */
+ size?: '3xs' | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
+
+ /**
+ * Controls gutter on the edges of the textarea
+ * Defaults to value from size
+ */
+ gutterSize?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+
+ /**
+ * Initial number of visible rows
+ * @default 3
+ */
+ rows?: number;
+
+ /**
+ * Automatically grow/shrink to fit content
+ * @default false
+ */
+ autoResize?: boolean;
+
+ /**
+ * Maximum number of rows when auto-resizing
+ * Defaults to Math.max(rows, 10)
+ */
+ maxRows?: number;
+
+ /**
+ * Allow password managers / autofill extensions to appear
+ * @default false
+ */
+ allowAutofillExtensions?: boolean;
+}
+
+// Size to text size mapping
+const textSizeMap: Record = {
+ '3xs': 'text-xs',
+ '2xs': 'text-xs',
+ xs: 'text-xs',
+ sm: 'text-sm',
+ md: 'text-sm',
+ lg: 'text-base',
+ xl: 'text-base',
+ '2xl': 'text-lg',
+ '3xl': 'text-lg',
+};
+
+// Gutter size mapping
+const gutterSizeMap: Record = {
+ '2xs': 'px-1.5 py-1',
+ xs: 'px-2 py-1.5',
+ sm: 'px-2.5 py-2',
+ md: 'px-3 py-2.5',
+ lg: 'px-3.5 py-3',
+ xl: 'px-4 py-3.5',
+};
+
+/**
+ * Textarea Component - For multi-line text input
+ *
+ * A customizable textarea component with optional auto-resize capability.
+ * Integrated with form submission and supports dynamic row adjustment.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Textarea",
+ * "name": "description",
+ * "placeholder": "Enter description",
+ * "rows": 4,
+ * "autoResize": true
+ * }
+ * ```
+ */
+export const Textarea: React.FC = ({
+ name,
+ defaultValue = '',
+ required = false,
+ placeholder,
+ autoSelect = false,
+ autoFocus = false,
+ disabled = false,
+ variant = 'outline',
+ size = 'md',
+ gutterSize,
+ rows = 3,
+ autoResize = false,
+ maxRows,
+ allowAutofillExtensions = false,
+}) => {
+ const textareaRef = useRef(null);
+ const [internalRows, setInternalRows] = useState(rows);
+
+ const maxRowsValue = maxRows || Math.max(rows, 10);
+
+ // Handle auto-select on mount
+ useEffect(() => {
+ if (autoSelect && textareaRef.current?.resizableTextArea?.textArea) {
+ textareaRef.current.resizableTextArea.textArea.select();
+ }
+ }, [autoSelect]);
+
+ // Handle value change
+ const handleChange = (e: React.ChangeEvent) => {
+ const newValue = e.currentTarget.value;
+ };
+
+ // Handle blur
+ const handleBlur = (e: React.FocusEvent) => {};
+
+ return (
+
+
+
+ );
+};
+
+export default Textarea;
diff --git a/web/src/components/widgets/Title.tsx b/web/src/components/widgets/Title.tsx
new file mode 100644
index 0000000..d69f56a
--- /dev/null
+++ b/web/src/components/widgets/Title.tsx
@@ -0,0 +1,188 @@
+import React from 'react';
+import { TextWeight, TextSize, TextColor } from '@/types';
+import useStyleBuilder from './styleBuilder';
+import { cn } from '@/lib/utils';
+import { resolveColorValue } from './colorUtils';
+
+export interface TitleProps {
+ /**
+ * Title text content to display
+ * Use either value or children, value takes precedence if both provided
+ * @example value="Page Title"
+ */
+ value?: string;
+
+ /**
+ * Child content to render
+ * Used if value is not provided
+ * @example children={Emphasized title }
+ */
+ children?: React.ReactNode;
+
+ /**
+ * Font weight of the title
+ * "normal" "medium" "semibold" "bold"
+ * @default "medium"
+ */
+ weight?: TextWeight;
+
+ /**
+ * Font size of the title
+ * "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl"
+ * @default "md"
+ * @example size="lg"
+ * @example size="2xl"
+ */
+ size?: TextSize;
+
+ /**
+ * Title color
+ * Accepts text color tokens (prose, primary, emphasis, secondary, tertiary, success, warning, danger),
+ * primitive color tokens (e.g. red-100, blue-900, gray-500), or CSS strings
+ * @default "prose"
+ * @example color="primary"
+ * @example color="red-500"
+ */
+ color?: TextColor | string;
+
+ /**
+ * Horizontal text alignment
+ * @default "start"
+ * @example textAlign="center"
+ */
+ textAlign?: 'start' | 'center' | 'end';
+
+ /**
+ * Truncate overflow with ellipsis
+ * Applies single-line truncation
+ * @default false
+ * @example truncate={true}
+ */
+ truncate?: boolean;
+
+ /**
+ * Limit text to a maximum number of lines
+ * Applies a line clamp
+ * @example maxLines={2}
+ */
+ maxLines?: number;
+
+ /**
+ * Additional CSS class names
+ * Merged with generated classes
+ * @example className="uppercase"
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ * Applied after CSS-in-JS styles
+ * @example style={{ textDecoration: "underline" }}
+ */
+ style?: React.CSSProperties;
+
+ /**
+ * HTML heading level (h1-h6)
+ * Determines which heading tag is rendered
+ * @default 3 (renders as h3)
+ * @example level={1} // renders
+ * @example level={2} // renders
+ */
+ level?: 1 | 2 | 3 | 4 | 5 | 6;
+}
+
+/**
+ * Title Component - For rendering heading content
+ *
+ * A semantic heading component that renders heading tags (h1-h6) with flexible styling.
+ * Supports font weight, size, color customization, and semantic HTML structure.
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ *
+ * @example
+ * ```json
+ * {
+ * "type": "Title",
+ * "value": "Main Title",
+ * "level": 1,
+ * "weight": "bold",
+ * "size": "lg",
+ * "color": "primary"
+ * }
+ * ```
+ */
+export const Title: React.FC = ({
+ value,
+ children,
+ size,
+ weight = 'medium',
+ color = 'prose',
+ className = '',
+ style = {},
+ level = 3,
+ truncate = false,
+ maxLines = undefined,
+ textAlign = 'start',
+ ...props
+}) => {
+ const { style: colorStyle, className: colorClassName } = resolveColorValue(color);
+
+ // Build font size, line height, and tracking (letter-spacing) CSS variables for heading styles
+ const fontSizeValue = size ? `var(--font-heading-${size}-size)` : undefined;
+ const lineHeightValue = size ? `var(--font-heading-${size}-line-height)` : undefined;
+ const trackingValue = size ? `var(--font-heading-${size}-tracking)` : undefined;
+ const fontWeightValue = weight ? `var(--font-weight-${weight})` : undefined;
+
+ const defaultStyle: React.CSSProperties = {
+ margin: 0,
+ ...(fontSizeValue && { fontSize: fontSizeValue }),
+ ...(lineHeightValue && { lineHeight: lineHeightValue }),
+ ...(trackingValue && { letterSpacing: trackingValue }),
+ ...(fontWeightValue && { fontWeight: fontWeightValue }),
+ ...(colorStyle && { color: colorStyle }),
+ ...(textAlign && { textAlign: textAlign }),
+ ...(truncate && {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ }),
+ ...(maxLines && {
+ display: '-webkit-box',
+ WebkitLineClamp: maxLines,
+ WebkitBoxOrient: 'vertical',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ }),
+ ...style,
+ };
+
+ const defaultClassName = cn(['w-title'], colorClassName, className);
+
+ const { style: titleStyle, className: titleClassName } = useStyleBuilder({
+ ...props,
+ style: defaultStyle,
+ className: defaultClassName,
+ });
+
+ const tags = {
+ 1: 'h1',
+ 2: 'h2',
+ 3: 'h3',
+ 4: 'h4',
+ 5: 'h5',
+ 6: 'h6',
+ } as const;
+
+ const HeadingTag = tags[level] as any;
+
+ return (
+
+ {value !== undefined ? value : children}
+
+ );
+};
+
+export default Title;
diff --git a/web/src/components/widgets/Transition.tsx b/web/src/components/widgets/Transition.tsx
new file mode 100644
index 0000000..b558d08
--- /dev/null
+++ b/web/src/components/widgets/Transition.tsx
@@ -0,0 +1,10 @@
+/**
+ * Transition Component - Animate layout changes for a single child component.
+ * @param children - The child component to animate layout changes for.
+ * @returns
+ */
+const Transition = ({ children }) => {
+ return children;
+};
+
+export default Transition;
diff --git a/web/src/components/widgets/badge.css b/web/src/components/widgets/badge.css
new file mode 100644
index 0000000..2555fc7
--- /dev/null
+++ b/web/src/components/widgets/badge.css
@@ -0,0 +1,170 @@
+[data-w-component='badge'] {
+ --indicator-size: var(--badge-indicator-size);
+ display: inline-flex;
+ align-items: center;
+ gap: calc(var(--spacing) * 1);
+ height: var(--badge-size);
+ padding: 0 var(--badge-gutter);
+ border-radius: var(--badge-radius);
+ font-size: var(--badge-font-size);
+ /* font-weight: var(--badge-font-weight); */
+ font-weight: var(--font-weight-normal);
+ letter-spacing: var(--badge-tracking);
+ white-space: nowrap;
+ align-self: flex-start;
+}
+
+[data-w-component='badge'] svg {
+ font-size: var(--badge-icon-font-size);
+}
+
+:where([data-w-component='badge'] svg:first-child:not(:only-child)) {
+ margin-left: var(--badge-icon-offset, -1px);
+}
+
+:where([data-w-component='badge'] svg:last-child:not(:only-child)) {
+ margin-right: var(--badge-icon-offset, -1px);
+}
+
+[data-w-component='badge'][data-variant='soft'] {
+ background-color: var(--badge-background-color);
+ color: var(--badge-text-color);
+}
+
+[data-w-component='badge'][data-variant='soft']:where([data-color='secondary']) {
+ --badge-background-color: var(--color-background-secondary-soft-alpha);
+ --badge-text-color: var(--color-text-secondary-soft);
+}
+
+[data-w-component='badge'][data-variant='soft']:where([data-color='success']) {
+ --badge-background-color: var(--color-background-success-soft-alpha);
+ --badge-text-color: var(--color-text-success-soft);
+}
+
+[data-w-component='badge'][data-variant='soft']:where([data-color='warning']) {
+ --badge-background-color: var(--color-background-warning-soft-alpha);
+ --badge-text-color: var(--color-text-warning-soft);
+}
+
+[data-w-component='badge'][data-variant='soft']:where([data-color='danger']) {
+ --badge-background-color: var(--color-background-danger-soft-alpha);
+ --badge-text-color: var(--color-text-danger-soft);
+}
+
+[data-w-component='badge'][data-variant='soft']:where([data-color='info']) {
+ --badge-background-color: var(--color-background-info-soft-alpha);
+ --badge-text-color: var(--color-text-info-soft);
+}
+
+[data-w-component='badge'][data-variant='soft']:where([data-color='discovery']) {
+ --badge-background-color: var(--color-background-discovery-soft-alpha);
+ --badge-text-color: var(--color-text-discovery-soft);
+}
+
+[data-w-component='badge'][data-variant='solid'] {
+ background-color: var(--badge-background-color);
+ color: var(--badge-text-color);
+}
+
+[data-w-component='badge'][data-variant='solid']:where([data-color='secondary']) {
+ --badge-background-color: var(--color-background-secondary-solid);
+ --badge-text-color: var(--color-text-secondary-solid);
+}
+
+[data-w-component='badge'][data-variant='solid']:where([data-color='success']) {
+ --badge-background-color: var(--color-background-success-solid);
+ --badge-text-color: var(--color-text-success-solid);
+}
+
+[data-w-component='badge'][data-variant='solid']:where([data-color='warning']) {
+ --badge-background-color: var(--color-background-warning-solid);
+ --badge-text-color: var(--color-text-warning-solid);
+}
+
+[data-w-component='badge'][data-variant='solid']:where([data-color='danger']) {
+ --badge-background-color: var(--color-background-danger-solid);
+ --badge-text-color: var(--color-text-danger-solid);
+}
+
+[data-w-component='badge'][data-variant='solid']:where([data-color='info']) {
+ --badge-background-color: var(--color-background-info-solid);
+ --badge-text-color: var(--color-text-info-solid);
+}
+
+[data-w-component='badge'][data-variant='solid']:where([data-color='discovery']) {
+ --badge-background-color: var(--color-background-discovery-solid);
+ --badge-text-color: var(--color-text-discovery-solid);
+}
+
+[data-w-component='badge'][data-variant='outline'] {
+ box-shadow: 0 0 0 1px var(--badge-border-color) inset;
+ color: var(--badge-text-color);
+}
+
+[data-w-component='badge'][data-variant='outline']:where([data-color='secondary']) {
+ --badge-border-color: var(--color-border-secondary-outline);
+ --badge-text-color: var(--color-text-secondary-outline);
+}
+
+[data-w-component='badge'][data-variant='outline']:where([data-color='success']) {
+ --badge-border-color: var(--color-border-success-outline);
+ --badge-text-color: var(--color-text-success-outline);
+}
+
+[data-w-component='badge'][data-variant='outline']:where([data-color='warning']) {
+ --badge-border-color: var(--color-border-warning-outline);
+ --badge-text-color: var(--color-text-warning-outline);
+}
+
+[data-w-component='badge'][data-variant='outline']:where([data-color='danger']) {
+ --badge-border-color: var(--color-border-danger-outline);
+ --badge-text-color: var(--color-text-danger-outline);
+}
+
+[data-w-component='badge'][data-variant='outline']:where([data-color='info']) {
+ --badge-border-color: var(--color-border-info-outline);
+ --badge-text-color: var(--color-text-info-outline);
+}
+
+[data-w-component='badge'][data-variant='outline']:where([data-color='discovery']) {
+ --badge-border-color: var(--color-border-discovery-outline);
+ --badge-text-color: var(--color-text-discovery-outline);
+}
+
+[data-w-component='badge']:where([data-size='sm']) {
+ --badge-size: var(--badge-size-sm);
+ --badge-gutter: var(--badge-gutter-sm);
+ --badge-font-size: var(--badge-font-size-sm);
+ --badge-font-weight: var(--badge-font-weight-sm);
+ --badge-tracking: var(--badge-tracking-sm);
+ --badge-radius: var(--badge-radius-sm);
+ --badge-icon-font-size: var(--badge-icon-font-size-sm);
+ --badge-indicator-size: var(--badge-indicator-size-sm);
+}
+
+[data-w-component='badge']:where([data-size='md']) {
+ --badge-size: var(--badge-size-md);
+ --badge-gutter: var(--badge-gutter-md);
+ --badge-font-size: var(--badge-font-size-md);
+ --badge-font-weight: var(--badge-font-weight-md);
+ --badge-tracking: var(--badge-tracking-md);
+ --badge-radius: var(--badge-radius-md);
+ --badge-icon-font-size: var(--badge-icon-font-size-md);
+ --badge-indicator-size: var(--badge-indicator-size-md);
+}
+
+[data-w-component='badge']:where([data-size='lg']) {
+ --badge-size: var(--badge-size-lg);
+ --badge-gutter: var(--badge-gutter-lg);
+ --badge-font-size: var(--badge-font-size-lg);
+ --badge-font-weight: var(--badge-font-weight-lg);
+ --badge-tracking: var(--badge-tracking-lg);
+ --badge-radius: var(--badge-radius-lg);
+ --badge-icon-font-size: var(--badge-icon-font-size-lg);
+ --badge-indicator-size: var(--badge-indicator-size-lg);
+}
+
+[data-w-component='badge']:where([data-pill]) {
+ --badge-radius: var(--radius-full);
+ padding: 0 calc(var(--badge-gutter) * var(--control-gutter-pill-scaling));
+}
diff --git a/web/src/components/widgets/box.css b/web/src/components/widgets/box.css
new file mode 100644
index 0000000..b3e8c1d
--- /dev/null
+++ b/web/src/components/widgets/box.css
@@ -0,0 +1,42 @@
+[data-w-direction='row'] > :not([data-w-has-width]) {
+ /* min-width: 0; */
+}
+
+/* Box auto-spacing: first child resets margin */
+:where([data-w-direction='col'][data-w-auto-spacing]) > [data-w-component]:first-child {
+ margin-block-start: 0;
+}
+
+:where([data-w-direction='row'][data-w-auto-spacing]) > [data-w-component]:first-child {
+ margin-inline-start: 0;
+}
+
+/* Box auto-spacing: non-first children get spacing */
+:where([data-w-direction='col'][data-w-auto-spacing]) > [data-w-component]:not(:first-child) {
+ margin-block-start: calc(var(--spacing) * 4);
+}
+
+:where([data-w-direction='row'][data-w-auto-spacing]) > [data-w-component]:not(:first-child) {
+ margin-inline-start: calc(var(--spacing) * 4);
+}
+
+/* Flush the box to negate parent padding */
+ [data-w-direction=col]>[data-w-flush]:first-child {
+ margin-block-start: calc(-1 * var(--w-box-gutter-block-start));
+}
+ [data-w-direction=col]>[data-w-flush] {
+ margin-inline: calc(-1 * var(--w-box-gutter-inline-start)) calc(-1 * var(--w-box-gutter-inline-end));
+}
+
+[data-w-direction=row]>[data-w-flush]:first-child {
+ margin-inline-start: calc(-1 * var(--w-box-gutter-inline-start));
+}
+
+[data-w-direction=row]>[data-w-flush] {
+ margin-block: calc(-1 * var(--w-box-gutter-block-start));
+}
+
+/* Center the content of the box */
+[data-w-align=center] {
+ align-items: center;
+}
\ No newline at end of file
diff --git a/web/src/components/widgets/button.css b/web/src/components/widgets/button.css
new file mode 100644
index 0000000..8e5e30d
--- /dev/null
+++ b/web/src/components/widgets/button.css
@@ -0,0 +1,532 @@
+.shrink {
+ flex-shrink: 1;
+}
+
+[data-w-component='button'] {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--button-gap);
+ flex-shrink: 0;
+ height: var(--button-size);
+ padding: 0 var(--button-gutter);
+ border-radius: var(--button-radius);
+ border: none;
+ cursor: pointer;
+ font-size: var(--button-font-size);
+ font-weight: var(--button-font-weight);
+ line-height: 1;
+ transition-duration: var(--transition-duration-basic);
+ transition-property: opacity, color, background-color, border-color, box-shadow;
+ transition-timing-function: var(--transition-ease-basic);
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+ z-index: 0;
+}
+
+[data-w-component='button']:where([data-block]) {
+ width: 100%;
+}
+
+/* Button sizes */
+[data-w-component='button']:where([data-size='3xs']) {
+ --button-size: var(--control-size-3xs);
+ --button-gutter: var(--control-gutter-sm);
+ --button-font-size: var(--control-font-size-sm);
+ --button-icon-size: var(--control-icon-size-sm);
+ --button-gap: var(--button-gap-sm);
+ --button-radius: var(--control-radius-sm);
+}
+
+[data-w-component='button']:where([data-size='2xs']) {
+ --button-size: var(--control-size-2xs);
+ --button-gutter: var(--control-gutter-sm);
+ --button-font-size: var(--control-font-size-sm);
+ --button-icon-size: var(--control-icon-size-sm);
+ --button-gap: var(--button-gap-sm);
+ --button-radius: var(--control-radius-sm);
+}
+
+[data-w-component='button']:where([data-size='xs']) {
+ --button-size: var(--control-size-xs);
+ --button-gutter: var(--control-gutter-sm);
+ --button-font-size: var(--control-font-size-sm);
+ --button-icon-size: var(--control-icon-size-sm);
+ --button-gap: var(--button-gap-sm);
+ --button-radius: var(--control-radius-sm);
+}
+
+[data-w-component='button']:where([data-size='sm']) {
+ --button-size: var(--control-size-sm);
+ --button-gutter: var(--control-gutter-sm);
+ --button-font-size: var(--control-font-size-sm);
+ --button-icon-size: var(--control-icon-size-sm);
+ --button-gap: var(--button-gap-sm);
+ --button-radius: var(--control-radius-sm);
+}
+
+[data-w-component='button']:where([data-size='md']) {
+ --button-size: var(--control-size-md);
+ --button-gutter: var(--control-gutter-md);
+ --button-font-size: var(--control-font-size-md);
+ --button-icon-size: var(--control-icon-size-md);
+ --button-gap: var(--button-gap-md);
+ --button-radius: var(--control-radius-md);
+}
+
+[data-w-component='button']:where([data-size='lg']) {
+ --button-size: var(--control-size-lg);
+ --button-gutter: var(--control-gutter-md);
+ --button-font-size: var(--control-font-size-md);
+ --button-icon-size: var(--control-icon-size-md);
+ --button-gap: var(--button-gap-lg);
+ --button-radius: var(--control-radius-md);
+}
+
+[data-w-component='button']:where([data-size='xl']) {
+ --button-size: var(--control-size-xl);
+ --button-gutter: var(--control-gutter-lg);
+ --button-font-size: var(--control-font-size-lg);
+ --button-icon-size: var(--control-icon-size-lg);
+ --button-gap: var(--button-gap-lg);
+ --button-radius: var(--control-radius-lg);
+}
+
+[data-w-component='button']:where([data-size='2xl']) {
+ --button-size: var(--control-size-2xl);
+ --button-gutter: var(--control-gutter-lg);
+ --button-font-size: var(--control-font-size-lg);
+ --button-icon-size: var(--control-icon-size-lg);
+ --button-gap: var(--button-gap-lg);
+ --button-radius: var(--control-radius-lg);
+}
+
+[data-w-component='button']:where([data-size='3xl']) {
+ --button-size: var(--control-size-3xl);
+ --button-gutter: var(--control-gutter-lg);
+ --button-font-size: var(--control-font-size-lg);
+ --button-icon-size: var(--control-icon-size-lg);
+ --button-gap: var(--button-gap-lg);
+ --button-radius: var(--control-radius-lg);
+}
+
+/* Button pill shape */
+[data-w-component='button']:where([data-pill]) {
+ --button-radius: var(--radius-full);
+ padding: 0 calc(var(--button-gutter) * var(--control-gutter-pill-scaling));
+}
+
+/* Button uniform size (square) */
+[data-w-component='button']:where([data-uniform]) {
+ width: var(--button-size);
+ padding: 0;
+}
+
+/* Button background layers */
+[data-w-component='button']:before {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: block;
+ border-radius: inherit;
+ content: '';
+ transition-duration: var(--transition-duration-basic);
+ transition-property: opacity, background-color, transform, box-shadow, border-color;
+ transition-timing-function: var(--transition-ease-basic);
+ will-change: transform;
+ z-index: -1;
+}
+
+/* Ripple effect layer */
+[data-w-component='button']:after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: block;
+ border-radius: inherit;
+ content: '';
+ pointer-events: none;
+ transition-duration: var(--transition-duration-basic);
+ transition-property: transform;
+ transition-timing-function: var(--transition-ease-basic);
+ will-change: transform;
+ z-index: 0;
+}
+
+/* Button inner content */
+.w-button-inner {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: inherit;
+ width: 100%;
+ height: 100%;
+ transition: opacity 0.15s ease 0.1s;
+ z-index: 1;
+}
+
+/* Button icon base */
+.w-button-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ color: currentColor;
+}
+
+/* Disabled state */
+[data-w-component='button']:disabled,
+[data-w-component='button']:where([aria-disabled='true']) {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+/* ====================
+ SOLID VARIANT COLORS
+ ==================== */
+
+/* Solid Primary */
+[data-w-component='button'][data-variant='solid']:where([data-color='primary']) {
+ --button-background-color: var(--color-background-primary-solid);
+ --button-background-color-hover: var(--color-background-primary-solid-hover);
+ --button-background-color-active: var(--color-background-primary-solid-active);
+ --button-text-color: var(--color-text-primary-solid);
+}
+
+/* Solid Secondary */
+[data-w-component='button'][data-variant='solid']:where([data-color='secondary']) {
+ --button-background-color: var(--color-background-secondary-solid);
+ --button-background-color-hover: var(--color-background-secondary-solid-hover);
+ --button-background-color-active: var(--color-background-secondary-solid-active);
+ --button-text-color: var(--color-text-secondary-solid);
+}
+
+/* Solid Info */
+[data-w-component='button'][data-variant='solid']:where([data-color='info']) {
+ --button-background-color: var(--color-background-info-solid);
+ --button-background-color-hover: var(--color-background-info-solid-hover);
+ --button-background-color-active: var(--color-background-info-solid-active);
+ --button-text-color: var(--color-text-info-solid);
+}
+
+/* Solid Success */
+[data-w-component='button'][data-variant='solid']:where([data-color='success']) {
+ --button-background-color: var(--color-background-success-solid);
+ --button-background-color-hover: var(--color-background-success-solid-hover);
+ --button-background-color-active: var(--color-background-success-solid-active);
+ --button-text-color: var(--color-text-success-solid);
+}
+
+/* Solid Warning */
+[data-w-component='button'][data-variant='solid']:where([data-color='warning']) {
+ --button-background-color: var(--color-background-warning-solid);
+ --button-background-color-hover: var(--color-background-warning-solid-hover);
+ --button-background-color-active: var(--color-background-warning-solid-active);
+ --button-text-color: var(--color-text-warning-solid);
+}
+
+/* Solid Caution */
+[data-w-component='button'][data-variant='solid']:where([data-color='caution']) {
+ --button-background-color: var(--color-background-caution-solid);
+ --button-background-color-hover: var(--color-background-caution-solid-hover);
+ --button-background-color-active: var(--color-background-caution-solid-active);
+ --button-text-color: var(--color-text-caution-solid);
+}
+
+/* Solid Danger */
+[data-w-component='button'][data-variant='solid']:where([data-color='danger']) {
+ --button-background-color: var(--color-background-danger-solid);
+ --button-background-color-hover: var(--color-background-danger-solid-hover);
+ --button-background-color-active: var(--color-background-danger-solid-active);
+ --button-text-color: var(--color-text-danger-solid);
+}
+
+/* Solid Discovery */
+[data-w-component='button'][data-variant='solid']:where([data-color='discovery']) {
+ --button-background-color: var(--color-background-discovery-solid);
+ --button-background-color-hover: var(--color-background-discovery-solid-hover);
+ --button-background-color-active: var(--color-background-discovery-solid-active);
+ --button-text-color: var(--color-text-discovery-solid);
+}
+
+/* Solid variant base */
+[data-w-component='button'][data-variant='solid'] {
+ color: var(--button-text-color);
+}
+
+[data-w-component='button'][data-variant='solid']:before {
+ background-color: var(--button-background-color);
+}
+
+[data-w-component='button'][data-variant='solid']:hover:not(:disabled):not([aria-disabled='true']):before {
+ background-color: var(--button-background-color-hover);
+}
+
+[data-w-component='button'][data-variant='solid']:active:not(:disabled):not([aria-disabled='true']):before {
+ background-color: var(--button-background-color-active);
+}
+
+/* ====================
+ SOFT VARIANT COLORS
+ ==================== */
+
+/* Soft variant base */
+[data-w-component='button'][data-variant='soft'] {
+ color: var(--button-text-color);
+}
+
+[data-w-component='button'][data-variant='soft']:before {
+ background-color: var(--button-background-soft-color);
+}
+
+[data-w-component='button'][data-variant='soft']:hover:not(:disabled):not([aria-disabled='true']):before {
+ background-color: var(--button-background-soft-color-hover);
+}
+
+/* Soft Primary */
+[data-w-component='button'][data-variant='soft']:where([data-color='primary']) {
+ --button-text-color: var(--color-text-primary-solid);
+ --button-background-soft-color: var(--color-background-primary-soft);
+ --button-background-soft-color-hover: var(--color-background-primary-soft-hover);
+}
+
+/* Soft Secondary */
+[data-w-component='button'][data-variant='soft']:where([data-color='secondary']) {
+ --button-text-color: var(--color-text-secondary-soft);
+ --button-background-soft-color: var(--color-background-secondary-soft);
+ --button-background-soft-color-hover: var(--color-background-secondary-soft-hover);
+}
+
+/* Soft Info */
+[data-w-component='button'][data-variant='soft']:where([data-color='info']) {
+ --button-text-color: var(--color-text-info-soft);
+ --button-background-soft-color: var(--color-background-info-soft);
+ --button-background-soft-color-hover: var(--color-background-info-soft-hover);
+}
+
+/* Soft Success */
+[data-w-component='button'][data-variant='soft']:where([data-color='success']) {
+ --button-text-color: var(--color-text-success-soft);
+ --button-background-soft-color: var(--color-background-success-soft);
+ --button-background-soft-color-hover: var(--color-background-success-soft-hover);
+}
+
+/* Soft Warning */
+[data-w-component='button'][data-variant='soft']:where([data-color='warning']) {
+ --button-text-color: var(--color-text-warning-soft);
+ --button-background-soft-color: var(--color-background-warning-soft);
+ --button-background-soft-color-hover: var(--color-background-warning-soft-hover);
+}
+
+/* Soft Caution */
+[data-w-component='button'][data-variant='soft']:where([data-color='caution']) {
+ --button-text-color: var(--color-text-caution-soft);
+ --button-background-soft-color: var(--color-background-caution-soft);
+ --button-background-soft-color-hover: var(--color-background-caution-soft-hover);
+}
+
+/* Soft Danger */
+[data-w-component='button'][data-variant='soft']:where([data-color='danger']) {
+ --button-text-color: var(--color-text-danger-soft);
+ --button-background-soft-color: var(--color-background-danger-soft);
+ --button-background-soft-color-hover: var(--color-background-danger-soft-hover);
+}
+
+/* Soft Discovery */
+[data-w-component='button'][data-variant='soft']:where([data-color='discovery']) {
+ --button-text-color: var(--color-text-discovery-soft);
+ --button-background-soft-color: var(--color-background-discovery-soft);
+ --button-background-soft-color-hover: var(--color-background-discovery-soft-hover);
+}
+
+/* ====================
+ OUTLINE VARIANT COLORS
+ ==================== */
+
+/* Outline variant base */
+[data-w-component='button'][data-variant='outline'] {
+ --button-ring-offset: -1px;
+ color: var(--button-text-color);
+}
+
+[data-w-component='button'][data-variant='outline']:before {
+ background-color: transparent;
+ box-shadow: 0 0 0 1px var(--button-border-color) inset;
+}
+
+[data-w-component='button'][data-variant='outline']:hover:not(:disabled):not([aria-disabled='true']):before {
+ box-shadow: 0 0 0 1px var(--button-border-color-hover) inset;
+ background-color: var(--button-background-outline-hover);
+}
+
+/* Outline Primary */
+[data-w-component='button'][data-variant='outline']:where([data-color='primary']) {
+ --button-text-color: var(--color-text-primary-outline);
+ --button-border-color: var(--color-border-primary-outline);
+ --button-border-color-hover: var(--color-border-primary-outline-hover);
+ --button-background-outline-hover: var(--color-background-primary-outline-hover);
+}
+
+/* Outline Secondary */
+[data-w-component='button'][data-variant='outline']:where([data-color='secondary']) {
+ --button-text-color: var(--color-text-secondary-outline);
+ --button-border-color: var(--color-border-secondary-outline);
+ --button-border-color-hover: var(--color-border-secondary-outline-hover);
+ --button-background-outline-hover: var(--color-background-secondary-outline-hover);
+}
+
+/* Outline Info */
+[data-w-component='button'][data-variant='outline']:where([data-color='info']) {
+ --button-text-color: var(--color-text-info-outline);
+ --button-border-color: var(--color-border-info-outline);
+ --button-border-color-hover: var(--color-border-info-outline-hover);
+ --button-background-outline-hover: var(--color-background-info-outline-hover);
+}
+
+/* Outline Success */
+[data-w-component='button'][data-variant='outline']:where([data-color='success']) {
+ --button-text-color: var(--color-text-success-outline);
+ --button-border-color: var(--color-border-success-outline);
+ --button-border-color-hover: var(--color-border-success-outline-hover);
+ --button-background-outline-hover: var(--color-background-success-outline-hover);
+}
+
+/* Outline Warning */
+[data-w-component='button'][data-variant='outline']:where([data-color='warning']) {
+ --button-text-color: var(--color-text-warning-outline);
+ --button-border-color: var(--color-border-warning-outline);
+ --button-border-color-hover: var(--color-border-warning-outline-hover);
+ --button-background-outline-hover: var(--color-background-warning-outline-hover);
+}
+
+/* Outline Caution */
+[data-w-component='button'][data-variant='outline']:where([data-color='caution']) {
+ --button-text-color: var(--color-text-caution-outline);
+ --button-border-color: var(--color-border-caution-outline);
+ --button-border-color-hover: var(--color-border-caution-outline-hover);
+ --button-background-outline-hover: var(--color-background-caution-outline-hover);
+}
+
+/* Outline Danger */
+[data-w-component='button'][data-variant='outline']:where([data-color='danger']) {
+ --button-text-color: var(--color-text-danger-outline);
+ --button-border-color: var(--color-border-danger-outline);
+ --button-border-color-hover: var(--color-border-danger-outline-hover);
+ --button-background-outline-hover: var(--color-background-danger-outline-hover);
+}
+
+/* Outline Discovery */
+[data-w-component='button'][data-variant='outline']:where([data-color='discovery']) {
+ --button-text-color: var(--color-text-discovery-outline);
+ --button-border-color: var(--color-border-discovery-outline);
+ --button-border-color-hover: var(--color-border-discovery-outline-hover);
+ --button-background-outline-hover: var(--color-background-discovery-outline-hover);
+}
+
+/* ====================
+ GHOST VARIANT COLORS
+ ==================== */
+
+/* Ghost variant base */
+[data-w-component='button'][data-variant='ghost'] {
+ color: var(--button-text-color);
+}
+
+[data-w-component='button'][data-variant='ghost']:before {
+ background-color: transparent;
+}
+
+[data-w-component='button'][data-variant='ghost']:hover:not(:disabled):not([aria-disabled='true']):before {
+ background-color: var(--button-background-ghost-hover);
+}
+
+/* Ghost Primary */
+[data-w-component='button'][data-variant='ghost']:where([data-color='primary']) {
+ --button-text-color: var(--color-text-primary-ghost);
+ --button-background-ghost-hover: var(--color-background-primary-ghost-hover);
+}
+
+/* Ghost Secondary */
+[data-w-component='button'][data-variant='ghost']:where([data-color='secondary']) {
+ --button-text-color: var(--color-text-secondary-ghost);
+ --button-background-ghost-hover: var(--color-background-secondary-ghost-hover);
+}
+
+/* Ghost Info */
+[data-w-component='button'][data-variant='ghost']:where([data-color='info']) {
+ --button-text-color: var(--color-text-info-ghost);
+ --button-background-ghost-hover: var(--color-background-info-ghost-hover);
+}
+
+/* Ghost Success */
+[data-w-component='button'][data-variant='ghost']:where([data-color='success']) {
+ --button-text-color: var(--color-text-success-ghost);
+ --button-background-ghost-hover: var(--color-background-success-ghost-hover);
+}
+
+/* Ghost Warning */
+[data-w-component='button'][data-variant='ghost']:where([data-color='warning']) {
+ --button-text-color: var(--color-text-warning-ghost);
+ --button-background-ghost-hover: var(--color-background-warning-ghost-hover);
+}
+
+/* Ghost Caution */
+[data-w-component='button'][data-variant='ghost']:where([data-color='caution']) {
+ --button-text-color: var(--color-text-caution-ghost);
+ --button-background-ghost-hover: var(--color-background-caution-ghost-hover);
+}
+
+/* Ghost Danger */
+[data-w-component='button'][data-variant='ghost']:where([data-color='danger']) {
+ --button-text-color: var(--color-text-danger-ghost);
+ --button-background-ghost-hover: var(--color-background-danger-ghost-hover);
+}
+
+/* Ghost Discovery */
+[data-w-component='button'][data-variant='ghost']:where([data-color='discovery']) {
+ --button-text-color: var(--color-text-discovery-ghost);
+ --button-background-ghost-hover: var(--color-background-discovery-ghost-hover);
+}
+
+/* ====================
+ ICON SIZE SUPPORT
+ ==================== */
+
+[data-w-component='button'][data-icon-size='sm'] .w-button-icon {
+ width: 0.875rem;
+ height: 0.875rem;
+ font-size: 0.875rem;
+}
+
+[data-w-component='button'][data-icon-size='md'] .w-button-icon {
+ width: 1rem;
+ height: 1rem;
+ font-size: 1rem;
+}
+
+[data-w-component='button'][data-icon-size='lg'] .w-button-icon {
+ width: 1.25rem;
+ height: 1.25rem;
+ font-size: 1.25rem;
+}
+
+[data-w-component='button'][data-icon-size='xl'] .w-button-icon {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 1.5rem;
+}
+
+[data-w-component='button'][data-icon-size='2xl'] .w-button-icon {
+ width: 2rem;
+ height: 2rem;
+ font-size: 2rem;
+}
diff --git a/web/src/components/widgets/card.css b/web/src/components/widgets/card.css
new file mode 100644
index 0000000..5a35063
--- /dev/null
+++ b/web/src/components/widgets/card.css
@@ -0,0 +1,30 @@
+[data-w-component='card'] {
+ --smoothing-background-color: var(--color-surface-elevated);
+ overflow: hidden;
+ width: 560px;
+ max-width: 100%;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-3xl);
+ background-color: var(--color-surface-elevated);
+ transition-duration: 0.6s;
+ transition-property: opacity, box-shadow;
+ transition-timing-function: ease;
+ will-change: opacity;
+}
+
+[data-w-component='card'][data-size='sm'] {
+ width: 360px;
+}
+
+[data-w-component='card'][data-size='md'] {
+ width: 440px;
+}
+
+[data-w-component='card'][data-size='lg'] {
+ width: 560px;
+}
+
+[data-w-component='card'][data-size='full'] {
+ width: 100%;
+}
+
diff --git a/web/src/components/widgets/colorUtils.test.ts b/web/src/components/widgets/colorUtils.test.ts
new file mode 100644
index 0000000..a38182a
--- /dev/null
+++ b/web/src/components/widgets/colorUtils.test.ts
@@ -0,0 +1,40 @@
+import { describe, it, expect } from 'vitest';
+import { hexToRgb, convertGradientHexToRgb } from './colorUtils';
+
+describe('Color Utils', () => {
+ describe('hexToRgb', () => {
+ it('should convert 6-digit hex to rgb', () => {
+ expect(hexToRgb('#378CD1')).toBe('rgb(55, 140, 209)');
+ expect(hexToRgb('#2B67AC')).toBe('rgb(43, 103, 172)');
+ });
+
+ it('should convert 3-digit hex to rgb', () => {
+ expect(hexToRgb('#fff')).toBe('rgb(255, 255, 255)');
+ expect(hexToRgb('#000')).toBe('rgb(0, 0, 0)');
+ });
+
+ it('should handle hex without # prefix', () => {
+ expect(hexToRgb('378CD1')).toBe('rgb(55, 140, 209)');
+ });
+ });
+
+ describe('convertGradientHexToRgb', () => {
+ it('should convert linear-gradient with hex colors', () => {
+ const input = 'linear-gradient(135deg, #378CD1 0%, #2B67AC 100%)';
+ const output = 'linear-gradient(135deg, rgb(55, 140, 209) 0%, rgb(43, 103, 172) 100%)';
+ expect(convertGradientHexToRgb(input)).toBe(output);
+ });
+
+ it('should handle radial-gradient', () => {
+ const input = 'radial-gradient(circle, #fff 0%, #000 100%)';
+ const output = 'radial-gradient(circle, rgb(255, 255, 255) 0%, rgb(0, 0, 0) 100%)';
+ expect(convertGradientHexToRgb(input)).toBe(output);
+ });
+
+ it('should handle gradient with multiple colors', () => {
+ const input = 'linear-gradient(90deg, #ff0000 0%, #00ff00 50%, #0000ff 100%)';
+ const output = 'linear-gradient(90deg, rgb(255, 0, 0) 0%, rgb(0, 255, 0) 50%, rgb(0, 0, 255) 100%)';
+ expect(convertGradientHexToRgb(input)).toBe(output);
+ });
+ });
+});
diff --git a/web/src/components/widgets/colorUtils.ts b/web/src/components/widgets/colorUtils.ts
new file mode 100644
index 0000000..bf2f679
--- /dev/null
+++ b/web/src/components/widgets/colorUtils.ts
@@ -0,0 +1,152 @@
+/**
+ * Color utilities for Text and Title components
+ * Handles multiple color formats: text color tokens, primitive color tokens, and CSS colors
+ */
+
+/**
+ * Text color tokens that map to CSS variables
+ */
+const TEXT_COLOR_TOKENS = [
+ 'prose',
+ 'primary',
+ 'emphasis',
+ 'secondary',
+ 'tertiary',
+ 'success',
+ 'warning',
+ 'danger',
+] as const;
+
+/**
+ * Check if a color value is a text color token
+ * @param color The color value to check
+ * @returns True if the color is a valid text color token
+ */
+export function isTextColorToken(color: string): color is (typeof TEXT_COLOR_TOKENS)[number] {
+ return TEXT_COLOR_TOKENS.includes(color as any);
+}
+
+/**
+ * Check if a color value is a primitive color token (Tailwind)
+ * Primitive color tokens follow the pattern: colorname-xxx (e.g., red-100, blue-900, gray-500)
+ * @param color The color value to check
+ * @returns True if the color looks like a primitive color token
+ */
+export function isPrimitiveColorToken(color: string): boolean {
+ return /^[a-z]+-\d{1,3}$/.test(color);
+}
+
+/**
+ * Check if a color value is a CSS color (hex, rgb, or named)
+ * Matches: hex (#112233, #fff), rgb(1,2,3), and CSS named colors (blue, red, etc)
+ * @param color The color value to check
+ * @returns True if the color looks like a CSS color
+ */
+export function isCSSColor(color: string): boolean {
+ // Hex color: #xxx or #xxxxxx
+ if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(color)) {
+ return true;
+ }
+
+ // rgb/rgba color: rgb(...) or rgba(...)
+ if (/^rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/.test(color)) {
+ return true;
+ }
+
+ // hsl/hsla color: hsl(...) or hsla(...)
+ if (/^hsla?\s*\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[\d.]+\s*)?\)$/.test(color)) {
+ return true;
+ }
+
+ // CSS named colors or currentColor
+ if (/^[a-z]+$/i.test(color)) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Convert a color value to a CSS color property value
+ * Supports three formats:
+ * 1. Text color tokens (prose, primary, etc) -> CSS variables
+ * 2. Primitive color tokens (red-100, blue-900) -> Tailwind classes
+ * 3. CSS colors (blue, #112233, rgb(1,2,3)) -> Direct CSS values
+ *
+ * @param color The color value to convert
+ * @returns { style?: string, className?: string } CSS property value or Tailwind class name
+ */
+export function resolveColorValue(color: string): { style?: string; className?: string } {
+ if (!color) {
+ return {};
+ }
+
+ // 1. Check if it's a text color token (maps to CSS variable)
+ if (isTextColorToken(color)) {
+ return {
+ style: `var(--color-text-${color})`,
+ };
+ }
+
+ // 2. Check if it's a primitive color token (red-100, blue-900, etc)
+ if (isPrimitiveColorToken(color)) {
+ return {
+ style: `var(--${color})`,
+ };
+ }
+
+ // 3. Check if it's a CSS color (hex, rgb, named colors)
+ if (isCSSColor(color)) {
+ return {
+ style: color,
+ };
+ }
+
+ // Fallback: treat as CSS color
+ return {
+ style: color,
+ };
+}
+
+/**
+ * Convert hex color to RGB format
+ * @param hex Hex color string (e.g., '#378CD1')
+ * @returns RGB color string (e.g., 'rgb(55, 140, 209)')
+ */
+export function hexToRgb(hex: string): string {
+ // Remove '#' if present
+ const cleanHex = hex.replace(/^#/, '');
+
+ // Handle 3-digit hex (#fff -> #ffffff)
+ let fullHex = cleanHex;
+ if (cleanHex.length === 3) {
+ fullHex = cleanHex.split('').map(char => char + char).join('');
+ }
+
+ // Parse hex values
+ const r = parseInt(fullHex.substring(0, 2), 16);
+ const g = parseInt(fullHex.substring(2, 4), 16);
+ const b = parseInt(fullHex.substring(4, 6), 16);
+
+ // Return RGB string
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+/**
+ * Convert linear-gradient with hex colors to RGB format
+ * @param gradientStr Gradient string (e.g., 'linear-gradient(135deg, #378CD1 0%, #2B67AC 100%)')
+ * @returns Gradient string with RGB colors
+ */
+export function convertGradientHexToRgb(gradientStr: string): string {
+ // Match hex colors in the gradient string and replace them with RGB
+ return gradientStr.replace(/#[0-9a-fA-F]{3,6}\b/g, (hex) => hexToRgb(hex));
+}
+
+export default {
+ isTextColorToken,
+ isPrimitiveColorToken,
+ isCSSColor,
+ resolveColorValue,
+ hexToRgb,
+ convertGradientHexToRgb,
+};
diff --git a/web/src/components/widgets/control.css b/web/src/components/widgets/control.css
new file mode 100644
index 0000000..768ebc1
--- /dev/null
+++ b/web/src/components/widgets/control.css
@@ -0,0 +1,249 @@
+/* Control sizes */
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='3xs']) {
+ --select-control-size: var(--control-size-3xs);
+ --select-control-gutter: var(--control-gutter-2xs);
+ --select-control-font-size: var(--control-font-size-sm);
+ --select-control-gap: 4px;
+ --select-control-radius: var(--control-radius-sm);
+ --start-icon-size: var(--control-icon-size-xs);
+ --start-icon-offset: 0;
+ --start-icon-pill-offset: 0;
+ --clear-size: 14px;
+ --clear-icon-size: 13px;
+ --clear-iso-size: 12px;
+ --clear-iso-icon-size: 10px;
+ --clear-iso-offset: -1px;
+ --clear-iso-pill-offset: -2px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: 0;
+ --dropdown-icon-width: 6px;
+ --dropdown-icon-height: 9px;
+ --dropdown-chevron-icon-width: 8px;
+ --dropdown-chevron-icon-height: 5px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1);
+ --indicator-wrapper-gutter: 0;
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='2xs']) {
+ --select-control-size: var(--control-size-2xs);
+ --select-control-gutter: var(--control-gutter-xs);
+ --select-control-font-size: var(--control-font-size-sm);
+ --select-control-gap: 6px;
+ --select-control-radius: var(--control-radius-sm);
+ --start-icon-size: var(--control-icon-size-sm);
+ --start-icon-offset: 0;
+ --start-icon-pill-offset: -1px;
+ --clear-size: 16px;
+ --clear-icon-size: 13px;
+ --clear-iso-size: 14px;
+ --clear-iso-icon-size: 12px;
+ --clear-iso-offset: -2px;
+ --clear-iso-pill-offset: -4px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: 0;
+ --dropdown-icon-width: 6px;
+ --dropdown-icon-height: 9px;
+ --dropdown-chevron-icon-width: 8px;
+ --dropdown-chevron-icon-height: 5px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1);
+ --indicator-wrapper-gutter: 0;
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='xs']) {
+ --select-control-size: var(--control-size-xs);
+ --select-control-gutter: var(--control-gutter-xs);
+ --select-control-font-size: var(--control-font-size-md);
+ --select-control-gap: 6px;
+ --select-control-radius: var(--control-radius-sm);
+ --start-icon-size: var(--control-icon-size-sm);
+ --start-icon-offset: -1px;
+ --start-icon-pill-offset: -1px;
+ --clear-size: 18px;
+ --clear-icon-size: 14px;
+ --clear-iso-size: 16px;
+ --clear-iso-icon-size: 12px;
+ --clear-iso-offset: -2px;
+ --clear-iso-pill-offset: -4px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: 0;
+ --dropdown-icon-width: 7px;
+ --dropdown-icon-height: 11px;
+ --dropdown-chevron-icon-width: 9px;
+ --dropdown-chevron-icon-height: 5px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1);
+ --indicator-wrapper-gutter: 0;
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='sm']) {
+ --select-control-size: var(--control-size-sm);
+ --select-control-gutter: var(--control-gutter-sm);
+ --select-control-font-size: var(--control-font-size-md);
+ --select-control-gap: 8px;
+ --select-control-radius: var(--control-radius-sm);
+ --start-icon-size: var(--control-icon-size-sm);
+ --start-icon-offset: -1px;
+ --start-icon-pill-offset: -2px;
+ --clear-size: 18px;
+ --clear-icon-size: 14px;
+ --clear-iso-size: 16px;
+ --clear-iso-icon-size: 12px;
+ --clear-iso-offset: -3px;
+ --clear-iso-pill-offset: -6px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: -2px;
+ --dropdown-icon-width: 7px;
+ --dropdown-icon-height: 11px;
+ --dropdown-chevron-icon-width: 9px;
+ --dropdown-chevron-icon-height: 5px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1);
+ --indicator-wrapper-gutter: 0;
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='md']) {
+ --select-control-size: var(--control-size-md);
+ --select-control-gutter: var(--control-gutter-md);
+ --select-control-font-size: var(--control-font-size-md);
+ --select-control-gap: 8px;
+ --select-control-radius: var(--control-radius-md);
+ --start-icon-size: var(--control-icon-size-md);
+ --start-icon-offset: -1px;
+ --start-icon-pill-offset: -4px;
+ --clear-size: 22px;
+ --clear-icon-size: 15px;
+ --clear-iso-size: 18px;
+ --clear-iso-icon-size: 14px;
+ --clear-iso-offset: -4px;
+ --clear-iso-pill-offset: -7px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: -2px;
+ --dropdown-icon-width: 8px;
+ --dropdown-icon-height: 12px;
+ --dropdown-chevron-icon-width: 11px;
+ --dropdown-chevron-icon-height: 6px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1);
+ --indicator-wrapper-gutter: calc(var(--spacing) * 0);
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='lg']) {
+ --select-control-size: var(--control-size-lg);
+ --select-control-gutter: var(--control-gutter-md);
+ --select-control-font-size: var(--control-font-size-md);
+ --select-control-gap: 8px;
+ --select-control-radius: var(--control-radius-md);
+ --start-icon-size: var(--control-icon-size-md);
+ --start-icon-offset: -2px;
+ --start-icon-pill-offset: -4px;
+ --clear-size: 22px;
+ --clear-icon-size: 15px;
+ --clear-iso-size: 18px;
+ --clear-iso-icon-size: 14px;
+ --clear-iso-offset: -2px;
+ --clear-iso-pill-offset: -6px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: -2px;
+ --dropdown-icon-width: 8px;
+ --dropdown-icon-height: 12px;
+ --dropdown-chevron-icon-width: 11px;
+ --dropdown-chevron-icon-height: 6px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1.5);
+ --indicator-wrapper-gutter: calc(var(--spacing) * 0);
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='xl']) {
+ --select-control-size: var(--control-size-xl);
+ --select-control-gutter: var(--control-gutter-lg);
+ --select-control-font-size: var(--control-font-size-md);
+ --select-control-gap: 8px;
+ --select-control-radius: var(--control-radius-lg);
+ --start-icon-size: var(--control-icon-size-md);
+ --start-icon-offset: -2px;
+ --start-icon-pill-offset: -4px;
+ --clear-size: 24px;
+ --clear-icon-size: 16px;
+ --clear-iso-size: 18px;
+ --clear-iso-icon-size: 14px;
+ --clear-iso-offset: -2px;
+ --clear-iso-pill-offset: -7px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: -2px;
+ --dropdown-icon-width: 8px;
+ --dropdown-icon-height: 12px;
+ --dropdown-chevron-icon-width: 11px;
+ --dropdown-chevron-icon-height: 6px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1);
+ --indicator-wrapper-gutter: calc(var(--spacing) * 0.5);
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='2xl']) {
+ --select-control-size: var(--control-size-2xl);
+ --select-control-gutter: var(--control-gutter-lg);
+ --select-control-font-size: var(--control-font-size-lg);
+ --select-control-gap: 10px;
+ --select-control-radius: var(--control-radius-xl);
+ --start-icon-size: var(--control-icon-size-lg);
+ --start-icon-offset: -2px;
+ --start-icon-pill-offset: -4px;
+ --clear-size: 24px;
+ --clear-icon-size: 16px;
+ --clear-iso-size: 20px;
+ --clear-iso-icon-size: 15px;
+ --clear-iso-offset: -2px;
+ --clear-iso-pill-offset: -7px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: -2px;
+ --dropdown-icon-width: 8px;
+ --dropdown-icon-height: 12px;
+ --dropdown-chevron-icon-width: 11px;
+ --dropdown-chevron-icon-height: 6px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1.5);
+ --indicator-wrapper-gutter: calc(var(--spacing) * 1);
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-size='3xl']) {
+ --select-control-size: var(--control-size-3xl);
+ --select-control-gutter: var(--control-gutter-xl);
+ --select-control-font-size: var(--control-font-size-lg);
+ --select-control-gap: 10px;
+ --select-control-radius: var(--control-radius-xl);
+ --start-icon-size: var(--control-icon-size-lg);
+ --start-icon-offset: -2px;
+ --start-icon-pill-offset: -4px;
+ --clear-size: 24px;
+ --clear-icon-size: 16px;
+ --clear-iso-size: 20px;
+ --clear-iso-icon-size: 15px;
+ --clear-iso-offset: -2px;
+ --clear-iso-pill-offset: -7px;
+ --dropdown-icon-offset: 0;
+ --dropdown-icon-pill-offset: -3px;
+ --dropdown-icon-width: 9px;
+ --dropdown-icon-height: 13px;
+ --dropdown-chevron-icon-width: 12px;
+ --dropdown-chevron-icon-height: 7px;
+ --indicator-wrapper-gap: calc(var(--spacing) * 1.5);
+ --indicator-wrapper-gutter: calc(var(--spacing) * 1);
+}
+
+/* control pill */
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-pill]) {
+ --select-control-radius: var(--radius-full);
+}
+/* control soft variant */
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-variant='soft']) {
+ --select-control-background-color: var(--color-background-primary-soft-alpha);
+}
+
+/* control outline variant */
+:is([data-w-component='select'], [data-w-component='date-picker']):where([data-variant='outline']):where(
+ :not([data-disabled])
+ ):hover {
+ --select-control-border-color: var(--input-outline-border-color-hover);
+}
+
+:is([data-w-component='select'], [data-w-component='date-picker'])[data-disabled][data-variant] {
+ --select-control-background-color: var(--color-background-disabled);
+ --select-control-border-color: var(--color-border-disabled);
+ color: var(--color-text-disabled);
+ cursor: not-allowed;
+}
diff --git a/web/src/components/widgets/iconMapping.ts b/web/src/components/widgets/iconMapping.ts
new file mode 100644
index 0000000..03e3a13
--- /dev/null
+++ b/web/src/components/widgets/iconMapping.ts
@@ -0,0 +1,85 @@
+import { ButtonIcon } from '@/types';
+
+/**
+ * Mapping from ButtonIcon names to lucide-react icon component names
+ * This allows us to use consistent ButtonIcon names while leveraging lucide-react's icon library
+ */
+export const buttonIconMap: Record = {
+ agent: 'Users',
+ analytics: 'BarChart',
+ atom: 'Atom',
+ batch: 'Grid',
+ bolt: 'Zap',
+ 'book-open': 'BookOpen',
+ 'book-closed': 'Book',
+ 'book-clock': 'Clock',
+ bug: 'Bug',
+ calendar: 'Calendar',
+ chart: 'BarChart3',
+ check: 'Check',
+ 'check-circle': 'CheckCircle',
+ 'check-circle-filled': 'CheckCircle2',
+ 'chevron-left': 'ChevronLeft',
+ 'chevron-right': 'ChevronRight',
+ 'circle-question': 'HelpCircle',
+ compass: 'Compass',
+ confetti: 'Sparkles',
+ cube: 'Cube',
+ desktop: 'Monitor',
+ document: 'FileText',
+ dot: 'CircleDot',
+ 'dots-horizontal': 'MoreHorizontal',
+ 'dots-vertical': 'MoreVertical',
+ 'empty-circle': 'Circle',
+ 'external-link': 'ExternalLink',
+ globe: 'Globe',
+ keys: 'Key',
+ lab: 'Beaker',
+ images: 'Image',
+ info: 'Info',
+ lifesaver: 'LifeBuoy',
+ lightbulb: 'Lightbulb',
+ mail: 'Mail',
+ 'map-pin': 'MapPin',
+ maps: 'Map',
+ mobile: 'Smartphone',
+ name: 'User',
+ notebook: 'Notebook',
+ 'notebook-pencil': 'NotebookPen',
+ 'page-blank': 'File',
+ phone: 'Phone',
+ play: 'Play',
+ plus: 'Plus',
+ profile: 'User',
+ 'profile-card': 'Card',
+ reload: 'RefreshCw',
+ star: 'Star',
+ 'star-filled': 'Star',
+ search: 'Search',
+ sparkle: 'Sparkles',
+ 'sparkle-double': 'Zap',
+ 'square-code': 'CodeSquare',
+ 'square-image': 'ImageSquare',
+ 'square-text': 'TextSquare',
+ suitcase: 'Briefcase',
+ 'settings-slider': 'Sliders',
+ user: 'User',
+ wreath: 'Wreath',
+ write: 'Pen',
+ 'write-alt': 'Edit',
+ 'write-alt2': 'Pencil',
+};
+
+/**
+ * Get lucide-react icon component name from ButtonIcon name
+ * @param iconName - ButtonIcon name
+ * @returns Lucide-react icon component name in PascalCase
+ * @example
+ * getLucideIconName('plus') // returns 'Plus'
+ * getLucideIconName('check-circle') // returns 'CheckCircle'
+ */
+export function getLucideIconName(iconName: ButtonIcon): string {
+ return buttonIconMap[iconName];
+}
+
+export default buttonIconMap;
diff --git a/web/src/components/widgets/image.css b/web/src/components/widgets/image.css
new file mode 100644
index 0000000..1800bde
--- /dev/null
+++ b/web/src/components/widgets/image.css
@@ -0,0 +1,28 @@
+/* Image Component */
+[data-w-component='image'] {
+ position: relative;
+ overflow: hidden;
+ border-radius: var(--radius-md);
+ align-items: stretch;
+ justify-content: flex-start;
+}
+
+/* Image sizing rules for row layouts */
+[data-w-direction='row'] > [data-w-component='image']:not([data-w-has-width][data-w-has-height]) {
+ aspect-ratio: 1;
+}
+
+[data-w-direction='row'] > [data-w-component='image']:not([data-w-has-width], [data-w-has-height]) {
+ width: 40px;
+}
+
+[data-w-direction='col'] > [data-w-component='image']:not([data-w-has-width][data-w-has-height]) {
+ aspect-ratio: 16 / 9;
+}
+
+[data-w-direction='col'] > [data-w-component='image'][data-w-flush]:first-child,
+[data-w-direction='col'] > [data-w-component='image'][data-w-flush]:first-child img,
+[data-w-direction='col'] > [data-w-component='image'][data-w-flush]:first-child > div {
+ border-end-end-radius: 0;
+ border-end-start-radius: 0;
+}
diff --git a/web/src/components/widgets/index.ts b/web/src/components/widgets/index.ts
new file mode 100644
index 0000000..befa478
--- /dev/null
+++ b/web/src/components/widgets/index.ts
@@ -0,0 +1,23 @@
+export { Card, type CardProps } from './Card';
+export { Box } from './Box';
+export { Row, type RowProps } from './Row';
+export { Col, type ColProps } from './Col';
+export { Text, type TextProps } from './Text';
+export { Title, type TitleProps } from './Title';
+export { Image, type ImageProps } from './Image';
+export { Button, type ButtonProps } from './Button';
+export { DatePicker, type DatePickerProps } from './DatePicker';
+export { Select, type SelectProps } from './Select';
+export { Checkbox, type CheckboxProps } from './Checkbox';
+export { RadioGroup, type RadioGroupProps } from './RadioGroup';
+export { Input, type InputProps } from './Input';
+export { Textarea, type TextareaProps } from './Textarea';
+export { Form, type FormProps } from './Form';
+export { Spacer, type SpacerProps } from './Spacer';
+export { Divider, type DividerProps } from './Divider';
+export { Caption, type CaptionProps } from './Caption';
+export { Label, type LabelProps } from './Label';
+export { Markdown, type MarkdownProps } from './Markdown';
+export { Icon, type IconProps } from './Icon';
+export { Chart, type ChartProps } from './Chart';
+export { Badge, type BadgeProps } from './Badge';
diff --git a/web/src/components/widgets/input.css b/web/src/components/widgets/input.css
new file mode 100644
index 0000000..86fc02b
--- /dev/null
+++ b/web/src/components/widgets/input.css
@@ -0,0 +1,300 @@
+[data-w-component='input'] > input:focus {
+ outline: none;
+}
+
+[data-w-component='input'] {
+ display: flex;
+ align-items: center;
+ gap: var(--input-gap);
+ width: 100%;
+ border-radius: var(--input-radius);
+ color: var(--input-text-color);
+ cursor: text;
+ font-size: var(--input-font-size);
+ font-weight: var(--font-weight-normal);
+ transition-duration: var(--transition-duration-basic);
+ transition-property: color, box-shadow, background-color;
+ transition-timing-function: var(--transition-ease-basic);
+}
+
+[data-w-component='input']:where([data-optically-align='start']) {
+ margin-inline-start: calc(var(--input-gutter) * -1);
+}
+
+[data-w-component='input']:where([data-optically-align='end']) {
+ margin-inline-end: calc(var(--input-gutter) * -1);
+}
+
+[data-w-component='input']:where([data-variant='outline']) {
+ box-shadow: 0 0 0 1px var(--input-outline-border-color) inset;
+}
+
+@media (hover: hover) and (pointer: fine) {
+ [data-w-component='input']:where([data-variant='outline']):not([data-disabled]):hover {
+ box-shadow: 0 0 0 1px var(--input-outline-border-color-hover) inset;
+ }
+}
+
+[data-w-component='input']:where([data-variant='outline']):not([data-disabled])[data-focused='true'] {
+ box-shadow: 0 0 0 1px var(--input-outline-border-color-focus) inset;
+}
+
+[data-w-component='input']:where([data-variant='outline']):not([data-disabled])[data-invalid] {
+ box-shadow: 0 0 0 1px var(--input-border-color-invalid) inset;
+}
+
+[data-w-component='input']:where([data-variant='soft']) {
+ background-color: var(--input-soft-background-color);
+}
+
+[data-w-component='input']:where([data-variant='soft']):not([data-disabled])[data-focused='true'] {
+ box-shadow: 0 0 0 1px var(--input-soft-border-color-focus) inset;
+}
+
+[data-w-component='input']:where([data-variant='soft']):not([data-disabled])[data-invalid] {
+ box-shadow: 0 0 0 1px var(--input-border-color-invalid) inset;
+}
+
+[data-w-component='input'][data-invalid] ::-moz-selection {
+ background-color: var(--red-a50);
+}
+
+[data-w-component='input'][data-invalid] ::selection {
+ background-color: var(--red-a50);
+}
+
+[data-w-component='input']:where([data-size='3xs']) {
+ --input-size: var(--control-size-3xs);
+ --input-font-size: var(--control-font-size-sm);
+ --input-radius: var(--control-radius-sm);
+ --input-gutter: var(--control-gutter-xs);
+ --input-gap: var(--input-gap-xs);
+ --input-adornment-offset: -1px;
+}
+
+[data-w-component='input']:where([data-size='2xs']) {
+ --input-size: var(--control-size-2xs);
+ --input-font-size: var(--control-font-size-sm);
+ --input-radius: var(--control-radius-sm);
+ --input-gutter: var(--control-gutter-xs);
+ --input-gap: var(--input-gap-xs);
+ --input-adornment-offset: -1px;
+}
+
+[data-w-component='input']:where([data-size='xs']) {
+ --input-size: var(--control-size-xs);
+ --input-font-size: var(--control-font-size-md);
+ --input-radius: var(--control-radius-sm);
+ --input-gutter: var(--control-gutter-xs);
+ --input-gap: var(--input-gap-sm);
+ --input-adornment-offset: -1px;
+}
+
+[data-w-component='input']:where([data-size='sm']) {
+ --input-size: var(--control-size-sm);
+ --input-font-size: var(--control-font-size-md);
+ --input-radius: var(--control-radius-md);
+ --input-gutter: var(--control-gutter-sm);
+ --input-gap: var(--input-gap-md);
+ --input-adornment-offset: -1px;
+}
+
+[data-w-component='input']:where([data-size='md']) {
+ --input-size: var(--control-size-md);
+ --input-font-size: var(--control-font-size-md);
+ --input-radius: var(--control-radius-md);
+ --input-gutter: var(--control-gutter-md);
+ --input-gap: var(--input-gap-md);
+}
+
+[data-w-component='input']:where([data-size='lg']) {
+ --input-size: var(--control-size-lg);
+ --input-font-size: var(--control-font-size-md);
+ --input-radius: var(--control-radius-md);
+ --input-gutter: var(--control-gutter-md);
+ --input-gap: var(--input-gap-md);
+}
+
+[data-w-component='input']:where([data-size='xl']) {
+ --input-size: var(--control-size-xl);
+ --input-font-size: var(--control-font-size-lg);
+ --input-radius: var(--control-radius-lg);
+ --input-gutter: var(--control-gutter-lg);
+ --input-gap: var(--input-gap-lg);
+}
+
+[data-w-component='input']:where([data-size='2xl']) {
+ --input-size: var(--control-size-2xl);
+ --input-font-size: var(--control-font-size-lg);
+ --input-radius: var(--control-radius-xl);
+ --input-gutter: var(--control-gutter-xl);
+ --input-gap: var(--input-gap-lg);
+ --input-adornment-offset: -4px;
+}
+
+[data-w-component='input']:where([data-size='3xl']) {
+ --input-size: var(--control-size-3xl);
+ --input-font-size: var(--control-font-size-lg);
+ --input-radius: var(--control-radius-xl);
+ --input-gutter: var(--control-gutter-xl);
+ --input-gap: var(--input-gap-lg);
+ --input-adornment-offset: -4px;
+}
+
+[data-w-component='input']:where([data-gutter-size='2xs']) {
+ --input-gutter: var(--control-gutter-2xs);
+ --input-adornment-offset: -1px;
+}
+
+[data-w-component='input']:where([data-gutter-size='xs']) {
+ --input-gutter: var(--control-gutter-xs);
+ --input-adornment-offset: -1px;
+}
+
+[data-w-component='input']:where([data-gutter-size='sm']) {
+ --input-gutter: var(--control-gutter-sm);
+ --input-adornment-offset: -2px;
+}
+
+[data-w-component='input']:where([data-gutter-size='md']) {
+ --input-gutter: var(--control-gutter-md);
+ --input-adornment-offset: -2px;
+}
+
+[data-w-component='input']:where([data-gutter-size='lg']) {
+ --input-gutter: var(--control-gutter-lg);
+ --input-adornment-offset: -2px;
+}
+
+[data-w-component='input']:where([data-gutter-size='xl']) {
+ --input-gutter: var(--control-gutter-xl);
+ --input-adornment-offset: -4px;
+}
+
+[data-w-component='input']:where([data-pill]) {
+ --input-radius: var(--radius-full);
+}
+
+[data-w-component='input'][data-disabled] {
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+[data-w-component='input']:where([data-has-start-adornment]) {
+ padding-left: var(--input-gutter);
+}
+
+[data-w-component='input']:where([data-has-start-adornment][data-pill]) {
+ padding-left: calc(var(--input-gutter) * var(--control-gutter-pill-scaling));
+}
+
+[data-w-component='input']:where([data-has-end-adornment]) {
+ padding-right: var(--input-gutter);
+}
+
+[data-w-component='input']:where([data-has-end-adornment][data-pill]) {
+ padding-right: calc(var(--input-gutter) * var(--control-gutter-pill-scaling));
+}
+
+[data-w-component='input'] > :first-child:not(input) {
+ margin-left: var(--input-adornment-offset, -2px);
+}
+
+[data-w-component='input'] > :last-child:not(input) {
+ margin-right: var(--input-adornment-offset, -2px);
+}
+
+[data-w-component='input'] > input {
+ display: block;
+ width: 100%;
+ height: var(--input-size);
+ padding: 0 var(--input-gutter);
+ border: 0;
+ border-radius: 0;
+ background: none;
+ box-shadow: none;
+ color: inherit;
+ font-size: inherit;
+ font-weight: inherit;
+ line-height: inherit;
+}
+
+[data-w-component='input'] > input:focus {
+ outline: 0;
+}
+
+[data-w-component='input'] > input:disabled {
+ cursor: not-allowed;
+}
+
+[data-w-component='input'] > input::-moz-placeholder {
+ color: var(--input-placeholder-text-color);
+}
+
+[data-w-component='input'] > input::placeholder {
+ color: var(--input-placeholder-text-color);
+}
+
+[data-w-component='input'] > input:-webkit-autofill {
+ animation: _native-autofill-in_1kh2t_1 1ms;
+}
+
+[data-w-component='input'] > input:-webkit-autofill,
+[data-w-component='input'] > input:-webkit-autofill:focus,
+[data-w-component='input'] > input:-webkit-autofill:hover {
+ color: var(--color-text) !important;
+ -webkit-text-fill-color: var(--color-text) !important;
+ -webkit-transition: background-color 0s 50000s, box-shadow 0s 50000s, filter 0s 50000s !important;
+ transition: background-color 0s 50000s, box-shadow 0s 50000s, filter 0s 50000s !important;
+}
+
+[data-w-component='input'] > input:-webkit-autofill-and-obscured,
+[data-w-component='input'] > input:-webkit-autofill-strong-password,
+[data-w-component='input'] > input:-webkit-autofill-strong-password-viewable {
+ color: var(--color-text) !important;
+ -webkit-text-fill-color: var(--color-text) !important;
+ -webkit-transition: background-color 0s 50000s, box-shadow 0s 50000s, filter 0s 50000s !important;
+ transition: background-color 0s 50000s, box-shadow 0s 50000s, filter 0s 50000s !important;
+}
+
+[data-w-component='input'] > input:-webkit-autofill:first-line {
+ font-size: inherit !important;
+ -webkit-transition: font-size 0s 50000s !important;
+ transition: font-size 0s 50000s !important;
+}
+
+[data-w-component='input'] > input[type='number'] {
+ -webkit-appearance: textfield;
+ -moz-appearance: textfield;
+ appearance: textfield;
+}
+
+[data-w-component='input'] > input[type='number']::-webkit-outer-spin-button,
+[data-w-component='input'] > input[type='number']::-webkit-inner-spin-button {
+ margin: 0;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+:where([data-w-component='input'][data-pill]) [data-w-component='input'] > input {
+ padding: 0 calc(var(--input-gutter) * var(--control-gutter-pill-scaling));
+}
+
+:where([data-w-component='input'][data-has-start-adornment]) [data-w-component='input'] > input {
+ padding-left: 0;
+}
+
+:where([data-w-component='input'][data-has-end-adornment]) [data-w-component='input'] > input {
+ padding-right: 0;
+}
+
+@keyframes _native-autofill-in_1kh2t_1 {
+ 0% {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
diff --git a/web/src/components/widgets/listview.css b/web/src/components/widgets/listview.css
new file mode 100644
index 0000000..4ea6138
--- /dev/null
+++ b/web/src/components/widgets/listview.css
@@ -0,0 +1,3 @@
+.text-secondary {
+ color: var(--color-text-secondary)
+}
\ No newline at end of file
diff --git a/web/src/components/widgets/styleBuilder.ts b/web/src/components/widgets/styleBuilder.ts
new file mode 100644
index 0000000..22c5665
--- /dev/null
+++ b/web/src/components/widgets/styleBuilder.ts
@@ -0,0 +1,283 @@
+import { useMemo } from 'react';
+import { cn } from '@/lib/utils';
+import { BackgroundColor } from '@/types';
+import { convertGradientHexToRgb } from './colorUtils';
+import { isNumber, isString } from 'lodash-es';
+
+/**
+ * StyleMap Configuration
+ * Note: Text color tokens have been moved to colorUtils.ts
+ */
+export const styleConfig = {
+ // Background Colour Map
+ backgroundColorMap: {
+ 'surface-tertiary': 'var(--color-surface-tertiary)',
+ 'surface-elevated': 'var(--color-surface-elevated)',
+ } as Record,
+};
+
+/**
+ * Process the colour values and convert them to the corresponding colour values CSS Variable or colour value
+ * @param colorValue Colour Value
+ * @returns CSS Colour values or variable references
+ */
+export function resolveColorValue(colorValue: string): string {
+ if (!colorValue) return colorValue;
+
+ // Check if it's a predefined background color token
+ if (styleConfig.backgroundColorMap[colorValue as BackgroundColor]) {
+ return styleConfig.backgroundColorMap[colorValue as BackgroundColor];
+ }
+ // Check if it's a gradient (contains 'gradient')
+ if (colorValue.includes('gradient')) {
+ return convertGradientHexToRgb(colorValue);
+ }
+ // Check if it's a surface color token (surface, surface-secondary, surface-tertiary, etc.)
+ if (colorValue === 'surface' || colorValue.startsWith('surface-')) {
+ return `var(--color-${colorValue})`;
+ }
+ // Check if it's a numbered color token like red-100, blue-900, gray-500
+ if (/^[a-z]+-\d+$/.test(colorValue)) {
+ return `var(--${colorValue})`;
+ }
+ // Otherwise treat as a CSS color value
+ return colorValue;
+}
+
+/**
+ * Style Build Parameter Interface
+ */
+export interface StyleBuilderProps {
+ // Layout Properties
+ /**
+ * padding: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
+ * padding: '10px' | '10px 20px' | '10px 20px 30px' | '10px 20px 30px 40px'
+ * padding: {
+ * x: 4,
+ * y: 8,
+ * }
+ */
+ padding?: string | number | object;
+ /**
+ * gap: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
+ */
+ gap?: string | number;
+ /**
+ * align: 'start' | 'center' | 'end' | 'baseline' | 'stretch'
+ */
+ align?: string;
+ /**
+ * justify: "start" | "center" | "end" | "stretch" | "between" | "around" | "evenly"
+ */
+ justify?: string;
+ /**
+ * 100%
+ * 150
+ */
+ width?: number | string;
+
+ /**
+ * 100%
+ * 150
+ */
+ height?: number | string;
+
+ /**
+ * Shorthand for width and height
+ * 40 | '100%'
+ */
+ size?: number | string;
+
+ minWidth?: number | string;
+ maxWidth?: number | string;
+ minHeight?: number | string;
+ maxHeight?: number | string;
+ direction?: 'row' | 'col';
+ /**
+ * wrap: "nowrap" | "wrap" | "wrap-reverse"
+ */
+ wrap?: string;
+ /**
+ * flex: '0 0 auto' | '1 1 auto' | '0 0 100px' | '0 1 200px'
+ */
+ flex?: string | number;
+
+ // Style Properties
+ /**
+ * blue-100 : from tailwind
+ * surface-tertiary: from css file
+ */
+ background?: string;
+ /**
+ * xl full ...
+ */
+ radius?: 'sm' | 'md' | 'lg' | 'full' | 'xl' | '2xl' | '2xs' | 'xs' | '3xl' | '4xl' | '100%' | 'none';
+ /**
+ * Border configuration
+ * @example { size: 1, color: "blue-400", style: "dashed" }
+ */
+ border?:
+ | number
+ | {
+ size?: number;
+ color?: string;
+ style?: 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
+ };
+
+ // Text Properties
+ /**
+ * medium 、 semibold ...
+ */
+ weight?: string;
+ color?: string;
+
+ // Other
+ className?: string;
+ style?: React.CSSProperties;
+}
+
+/**
+ * Return Value Type
+ */
+export interface StyleBuilderResult {
+ style: React.CSSProperties;
+ className: string;
+}
+
+/**
+ * General Style Builder
+ * For layout components (Box/Row/Col),UniformCSS-in-JSandTailwindClass Name
+ */
+export function useStyleBuilder(props: StyleBuilderProps): StyleBuilderResult {
+ const {
+ gap,
+ align,
+ justify,
+ width,
+ height,
+ size,
+ minWidth,
+ maxWidth,
+ minHeight,
+ maxHeight,
+ direction = 'col',
+ wrap,
+ flex,
+ background,
+ radius,
+ border,
+ className = '',
+ weight,
+ color,
+ style = {},
+ } = props;
+
+ // CSS-in-JS Style Objects
+ const computedStyle = useMemo(() => {
+ let backgroundStyle: string | undefined;
+ let smoothingBackgroundColor: string | undefined;
+
+ if (background) {
+ backgroundStyle = resolveColorValue(background);
+ // Set smoothing background color for surface tokens
+ if (background === 'surface' || background.startsWith('surface-')) {
+ smoothingBackgroundColor = backgroundStyle;
+ }
+ }
+
+ // Determine effective width and height
+ const effectiveWidth = width !== undefined ? width : size !== undefined ? size : undefined;
+ const effectiveHeight = height !== undefined ? height : size !== undefined ? size : undefined;
+
+ let gapStyle: React.CSSProperties = {};
+ if (gap) {
+ if (isNumber(gap)) {
+ gapStyle = { gap: `${Number(gap) * 0.25}rem` };
+ }
+ if (isString(gap)) {
+ gapStyle = { gap: gap };
+ }
+ }
+
+ // Build border style
+ let borderStyle: string | undefined;
+ if (border) {
+ if (typeof border === 'number') {
+ borderStyle = `${border}px solid`;
+ } else if (border.size && border.color && border.style) {
+ const resolvedColor = resolveColorValue(border.color);
+ borderStyle = `${border.size}px ${border.style} ${resolvedColor}`;
+ }
+ }
+
+ return {
+ display: 'flex',
+ flexDirection: direction === 'row' ? 'row' : 'column',
+ '--smoothing-background-color': smoothingBackgroundColor,
+ ...(backgroundStyle && { background: backgroundStyle }),
+ ...(borderStyle && { border: borderStyle }),
+ ...(wrap && { flexWrap: wrap as any }),
+ ...(effectiveWidth !== undefined && {
+ width: typeof effectiveWidth === 'number' ? `${effectiveWidth}px` : effectiveWidth,
+ }),
+ ...(effectiveHeight !== undefined && {
+ height: typeof effectiveHeight === 'number' ? `${effectiveHeight}px` : effectiveHeight,
+ }),
+ ...(minWidth !== undefined && { minWidth: `${minWidth}px` }),
+ ...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
+ ...(minHeight !== undefined && { minHeight: `${minHeight}px` }),
+ ...(maxHeight !== undefined && { maxHeight: `${maxHeight}px` }),
+ ...(flex && {
+ flex,
+ }),
+ // flexShrink: 0,
+
+ borderRadius: radius ? `var(--radius-${radius})` : undefined,
+ ...gapStyle,
+ ...style,
+ } as React.CSSProperties;
+ }, [
+ background,
+ border,
+ width,
+ size,
+ height,
+ gap,
+ direction,
+ wrap,
+ minWidth,
+ maxWidth,
+ minHeight,
+ maxHeight,
+ flex,
+ radius,
+ style,
+ ]);
+
+ // Tailwind classNames
+ const computedClassName = useMemo(() => {
+ const names = [];
+
+ if (background && !styleConfig.backgroundColorMap[background]) {
+ names.push(`bg-${background}`);
+ }
+ if (align) {
+ names.push(`items-${align}`);
+ }
+ if (justify) {
+ names.push(`justify-${justify}`);
+ }
+
+ if (weight) {
+ names.push(`font-${weight}`);
+ }
+ return cn(names, className);
+ }, [background, align, justify, weight, className]);
+
+ return {
+ style: computedStyle,
+ className: computedClassName,
+ };
+}
+
+export default useStyleBuilder;
diff --git a/web/src/components/widgets/text.css b/web/src/components/widgets/text.css
new file mode 100644
index 0000000..6d9c4d4
--- /dev/null
+++ b/web/src/components/widgets/text.css
@@ -0,0 +1,3 @@
+[data-w-direction='col'][data-w-auto-spacing] > [data-w-component='text'] + [data-w-component='text'] {
+ margin-block-start: calc(var(--spacing) * 0);
+}
diff --git a/web/src/components/widgets/types.ts b/web/src/components/widgets/types.ts
new file mode 100644
index 0000000..57fb91a
--- /dev/null
+++ b/web/src/components/widgets/types.ts
@@ -0,0 +1,255 @@
+import React from 'react';
+import { BackgroundColor, BorderRadius } from '@/types';
+
+/**
+ * Common styleable props interface
+ * Used by layout components that support styling (Box, Row, Col, etc.)
+ * Extends React.PropsWithChildren to include children, className, and style support
+ */
+export interface StyleableProps extends React.PropsWithChildren {
+ /**
+ * Component type identifier for CSS class generation
+ * @default 'box'
+ */
+ component?: string;
+
+ // ============================================
+ // Layout Properties
+ // ============================================
+
+ /**
+ * Space between direct children (in rem units)
+ * @example gap={2} // generates gap: 2rem
+ */
+ gap?: number | string;
+
+ /**
+ * Inner padding (in rem units)
+ * @example padding={4} // generates padding: 4rem
+ * @example padding={{ y: 8, x: 4 }} :
+ * --w-box-gutter-block-start: 2rem;
+ * --w-box-gutter-block-end: 2rem;
+ * --w-box-gutter-inline-start: 1rem;
+ * --w-box-gutter-inline-end: 1rem;
+ * padding-block: 2rem;
+ * padding-inline: 1rem;
+ * @example padding={{ top: 8, bottom: 4, left: 2, right: 6 }} // supports side-specific padding values
+ * @example padding={{ x: 4, top: 6 }} // axis values mix with side overrides (side has higher priority)
+ * @example padding={{ x: 4, top: 4, bottom: 1 }} // mix with multiple side overrides
+ */
+ padding?: string | number | object;
+
+ /**
+ * Cross-axis alignment of children
+ * Aligns items on the axis perpendicular to the flex direction
+ * @example align="center" // centers items vertically in row, horizontally in col
+ */
+ align?: 'start' | 'center' | 'end' | 'stretch';
+
+ /**
+ * Main-axis distribution of children
+ * Distributes items along the primary flex direction
+ * @example justify="between" // space-between layout
+ */
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around';
+
+ /**
+ * Flex direction for content layout
+ * Determines whether items flow horizontally or vertically
+ * @default 'col'
+ * @example direction="row" // horizontal layout
+ * @example direction="col" // vertical layout
+ */
+ direction?: 'row' | 'col';
+
+ /**
+ * Wrap behavior for flex items
+ * Controls whether flex items wrap to multiple lines
+ * @default 'nowrap'
+ */
+ wrap?: 'wrap' | 'nowrap';
+
+ // ============================================
+ // Size Properties
+ // ============================================
+
+ /**
+ * Explicit width (in pixels or CSS string)
+ * @example width={360} // generates width: 360px
+ * @example width="100%" // CSS string passthrough
+ */
+ width?: number | string;
+
+ /**
+ * Minimum width constraint (in pixels)
+ * @example minWidth={280}
+ */
+ minWidth?: number | string;
+
+ /**
+ * Maximum width constraint (in pixels)
+ * @example maxWidth={800}
+ */
+ maxWidth?: number | string;
+
+ /**
+ * Explicit height (in pixels or CSS string)
+ * @example height={200} // generates height: 200px
+ * @example height="100%" // CSS string passthrough
+ */
+ height?: number | string;
+
+ /**
+ * Shorthand for setting both width and height (in pixels or CSS string)
+ * Sets width and height to the same value
+ * @example size={40} // generates width: 40px; height: 40px
+ * @example size="100%" // generates width: 100%; height: 100%
+ */
+ size?: number | string;
+
+ /**
+ * Minimum height constraint (in pixels)
+ * @example minHeight={200}
+ */
+ minHeight?: number | string;
+
+ /**
+ * Maximum height constraint (in pixels)
+ * @example maxHeight={600}
+ */
+ maxHeight?: number | string;
+
+ // ============================================
+ // Style Properties
+ // ============================================
+
+ /**
+ * Background color or gradient
+ * Supports:
+ * - Predefined color tokens: 'surface-tertiary', 'surface-elevated'
+ * - CSS gradients: 'linear-gradient(135deg, #378CD1 0%, #2B67AC 100%)'
+ * - CSS colors: '#fff', 'rgb(255, 0, 0)', 'blue'
+ * Hex colors in gradients will be automatically converted to RGB format
+ * @example background="surface-tertiary" // uses CSS variable
+ * @example background="linear-gradient(135deg, #378CD1 0%, #2B67AC 100%)" // converts hex to RGB
+ * @example background="blue-100" // uses CSS variable
+ */
+ background?: BackgroundColor;
+
+ /**
+ * Border radius preset
+ * Predefined rounded corner values
+ * @example radius="md" // moderate rounding
+ * @example radius="full" // fully rounded (9999px)
+ */
+ radius?: BorderRadius;
+
+ /**
+ * Border configuration
+ * Border applied to the container; accepts a numeric pixel value or a border object.
+ * @example border={1} // generates border-width: 1px;
+ * @example border={{ size: 1, color: "blue-400", style: "dashed" }} // generates border: 1px dashed var(--blue-400);
+ */
+ border?:
+ | number
+ | {
+ size?: number;
+ color?: string;
+ style?: 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
+ };
+
+ /**
+ * Flex growth/shrink factor
+ * Controls how flex items grow and shrink
+ * @example flex="1" // grow to fill available space
+ * @example flex="0 0 auto" // don't grow or shrink
+ */
+ flex?: string | number;
+
+ // ============================================
+ // HTML Properties
+ // ============================================
+
+ /**
+ * Child components to render inside the container
+ * Passed through React.PropsWithChildren
+ */
+ children?: React.ReactNode;
+
+ /**
+ * Additional CSS class names
+ * Merged with generated classes via cn() utility
+ * @example className="custom-class" // appended to generated classes
+ */
+ className?: string;
+
+ /**
+ * Inline CSS styles
+ * Applied after CSS-in-JS styles (will override)
+ * @example style={{ zIndex: 100 }}
+ */
+ style?: React.CSSProperties;
+
+ /**
+ * Theme attribute
+ * Sets the data-theme attribute for theme-related styling
+ * Only renders data-theme attribute if theme is provided
+ * @example theme="dark" // generates data-theme="dark"
+ */
+ theme?: string;
+}
+
+export default StyleableProps;
+
+export type ActionConfig = {
+ type: string;
+ payload?: Record;
+ handler?: 'server' | 'client';
+ loadingBehavior?: 'auto' | 'none' | 'self' | 'container';
+};
+
+/** Border radius token or CSS keyword/percentage. */
+export type RadiusValue = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | 'full' | '100%' | 'none';
+
+export type Margin = {
+ /** Top margin; accepts a spacing unit or CSS string. */
+ top?: number | string;
+ /** Right margin; accepts a spacing unit or CSS string. */
+ right?: number | string;
+ /** Bottom margin; accepts a spacing unit or CSS string. */
+ bottom?: number | string;
+ /** Left margin; accepts a spacing unit or CSS string. */
+ left?: number | string;
+ /** Horizontal margin; accepts a spacing unit or CSS string. */
+ x?: number | string;
+ /** Vertical margin; accepts a spacing unit or CSS string. */
+ y?: number | string;
+};
+
+/** Common layout props for block-like components. */
+export type BlockProps = {
+ /** Explicit height; accepts a numeric pixel value or a CSS string. */
+ height?: number | string;
+ /** Explicit width; accepts a numeric pixel value or a CSS string. */
+ width?: number | string;
+ /** Shorthand to set both width and height; accepts a numeric pixel value or a CSS string. */
+ size?: number | string;
+ /** Minimum height constraint; accepts a numeric pixel value or a CSS string. */
+ minHeight?: number | string;
+ /** Minimum width constraint; accepts a numeric pixel value or a CSS string. */
+ minWidth?: number | string;
+ /** Shorthand to set both minWidth and minHeight; accepts a numeric pixel value or a CSS string. */
+ minSize?: number | string;
+ /** Maximum height constraint; accepts a numeric pixel value or a CSS string. */
+ maxHeight?: number | string;
+ /** Maximum width constraint; accepts a numeric pixel value or a CSS string. */
+ maxWidth?: number | string;
+ /** Shorthand to set both maxWidth and maxHeight; accepts a numeric pixel value or a CSS string. */
+ maxSize?: number | string;
+ /** Aspect ratio of the box (e.g., 16/9); accepts a numeric value or a CSS string. */
+ aspectRatio?: number | string;
+ /** Border radius; accepts a radius token. */
+ radius?: RadiusValue;
+ /** Outer margin; accepts a spacing unit, a CSS string, or a margin object. */
+ margin?: number | string | Margin;
+};
diff --git a/web/src/components/widgets/widgets.css b/web/src/components/widgets/widgets.css
new file mode 100644
index 0000000..35821b1
--- /dev/null
+++ b/web/src/components/widgets/widgets.css
@@ -0,0 +1,1397 @@
+/* Widget System - Base Styles */
+
+@import url('./box.css');
+@import url('./image.css');
+@import url('./text.css');
+@import url('./button.css');
+@import url('./control.css');
+
+@layer theme {
+ .w-widget-scope {
+ color: var(--color-text);
+ --font-sans: 'OpenAI Sans', ui-sans-serif, system-ui, sans-serif;
+ --font-mono: 'SF Mono', 'SFMono-Regular', ui-monospace, consolas, liberation mono, menlo, monospace;
+ --spacing: 0.25rem;
+ --breakpoint-sm: 576px;
+ --breakpoint-md: 768px;
+ --breakpoint-lg: 1024px;
+ --breakpoint-xl: 1280px;
+ --breakpoint-2xl: 1536px;
+ --container-md: 28rem;
+ --default-transition-duration: 0.15s;
+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ --default-font-family: var(--font-sans);
+ --default-mono-font-family: var(--font-mono);
+ --breakpoint-xs: 380px;
+ --font-weight-normal: 400;
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+ --radius-2xs: 0.125rem;
+ --radius-xs: 0.25rem;
+ --radius-sm: 0.375rem;
+ --radius-md: 0.5rem;
+ --radius-lg: 0.5rem;
+ --radius-xl: 0.75rem;
+ --radius-2xl: 1rem;
+ --radius-3xl: 1.5rem;
+ --radius-4xl: 2rem;
+ --radius-full: 9999px;
+ --text-lg: var(--font-text-lg-size);
+ --text-lg--line-height: var(--font-text-lg-line-height);
+ --text-lg--font-weight: var(--font-text-lg-weight);
+ --text-lg--letter-spacing: var(--font-text-lg-tracking);
+ --text-base: var(--font-text-md-size);
+ --text-base--line-height: var(--font-text-md-line-height);
+ --text-base--font-weight: var(--font-text-md-weight);
+ --text-base--letter-spacing: var(--font-text-md-tracking);
+ --text-sm: var(--font-text-sm-size);
+ --text-sm--line-height: var(--font-text-sm-line-height);
+ --text-sm--font-weight: var(--font-text-sm-weight);
+ --text-sm--letter-spacing: var(--font-text-sm-tracking);
+ --text-xs: var(--font-text-xs-size);
+ --text-xs--line-height: var(--font-text-xs-line-height);
+ --text-xs--font-weight: var(--font-text-xs-weight);
+ --text-xs--letter-spacing: var(--font-text-xs-tracking);
+ --text-2xs: var(--font-text-2xs-size);
+ --text-2xs--line-height: var(--font-text-2xs-line-height);
+ --text-2xs--font-weight: var(--font-text-2xs-weight);
+ --text-2xs--letter-spacing: var(--font-text-2xs-tracking);
+ --text-3xs: var(--font-text-3xs-size);
+ --text-3xs--line-height: var(--font-text-3xs-line-height);
+ --text-3xs--font-weight: var(--font-text-3xs-weight);
+ --text-3xs--letter-spacing: var(--font-text-3xs-tracking);
+ --text-xl: var(--font-heading-md-size);
+ --text-xl--line-height: var(--font-heading-md-line-height);
+ --text-xl--font-weight: var(--font-text-lg-weight);
+ --text-xl--letter-spacing: var(--font-heading-md-tracking);
+ --text-2xl: var(--font-heading-lg-size);
+ --text-2xl--line-height: var(--font-heading-lg-line-height);
+ --text-2xl--font-weight: var(--font-text-lg-weight);
+ --text-2xl--letter-spacing: var(--font-heading-lg-tracking);
+ --text-3xl: var(--font-heading-xl-size);
+ --text-3xl--line-height: var(--font-heading-xl-line-height);
+ --text-3xl--font-weight: var(--font-text-lg-weight);
+ --text-3xl--letter-spacing: var(--font-heading-xl-tracking);
+ --tracking-wide: var(--font-tracking-wide);
+ --tracking-normal: var(--font-tracking-normal);
+ --tracking-tight: var(--font-tracking-tight);
+ --shadow-md: var(--shadow-200);
+ --shadow-hairline: var(--shadow-hairline);
+ --color-gray-0: var(--gray-0);
+ --color-gray-100: var(--gray-100);
+ --color-gray-150: var(--gray-150);
+ --color-gray-300: var(--gray-300);
+ --color-gray-350: var(--gray-350);
+ --color-gray-500: var(--gray-500);
+ --color-red-300: var(--red-300);
+ --color-red-500: var(--red-500);
+ --color-red-600: var(--red-600);
+ --color-alpha: var(--alpha-base);
+ --color-white: var(--white);
+ --color-black: var(--black);
+
+ border: unset;
+ }
+ .w-widget-scope,
+ :where([data-theme]) {
+ --gray-500: #5d5d5d;
+ --alpha-0: color-mix(in oklab, var(--alpha-base) 0%, transparent);
+ --alpha-02: color-mix(in oklab, var(--alpha-base) 2%, transparent);
+ --alpha-04: color-mix(in oklab, var(--alpha-base) 4%, transparent);
+ --alpha-05: color-mix(in oklab, var(--alpha-base) 5%, transparent);
+ --alpha-06: color-mix(in oklab, var(--alpha-base) 6%, transparent);
+ --alpha-08: color-mix(in oklab, var(--alpha-base) 8%, transparent);
+ --alpha-10: color-mix(in oklab, var(--alpha-base) 10%, transparent);
+ --alpha-12: color-mix(in oklab, var(--alpha-base) 12%, transparent);
+ --alpha-15: color-mix(in oklab, var(--alpha-base) 15%, transparent);
+ --alpha-16: color-mix(in oklab, var(--alpha-base) 16%, transparent);
+ --alpha-20: color-mix(in oklab, var(--alpha-base) 20%, transparent);
+ --alpha-25: color-mix(in oklab, var(--alpha-base) 25%, transparent);
+ --alpha-30: color-mix(in oklab, var(--alpha-base) 30%, transparent);
+ --alpha-35: color-mix(in oklab, var(--alpha-base) 35%, transparent);
+ --alpha-40: color-mix(in oklab, var(--alpha-base) 40%, transparent);
+ --alpha-50: color-mix(in oklab, var(--alpha-base) 50%, transparent);
+ --alpha-60: color-mix(in oklab, var(--alpha-base) 60%, transparent);
+ --alpha-70: color-mix(in oklab, var(--alpha-base) 70%, transparent);
+ --white: #fff;
+ --black: #000;
+ --green-25: #edfaf2;
+ --green-50: #d9f4e4;
+ --green-75: #b8ebcc;
+ --green-100: #8cdfad;
+ --green-200: #66d492;
+ --green-300: #40c977;
+ --green-400: #04b84c;
+ --green-500: #00a240;
+ --green-600: #008635;
+ --green-700: #00692a;
+ --green-800: #004f1f;
+ --green-900: #003716;
+ --green-950: #011c0b;
+ --green-1000: #001207;
+ --green-a25: color-mix(in oklab, var(--green-400) 8%, transparent);
+ --green-a50: color-mix(in oklab, var(--green-400) 15%, transparent);
+ --green-a75: color-mix(in oklab, var(--green-400) 29%, transparent);
+ --green-a100: color-mix(in oklab, var(--green-400) 45%, transparent);
+ --green-a200: color-mix(in oklab, var(--green-400) 60%, transparent);
+ --green-a300: color-mix(in oklab, var(--green-400) 75%, transparent);
+ --red-25: #fff0f0;
+ --red-50: #ffd9d9;
+ --red-75: #ffc6c5;
+ --red-100: #ffa4a2;
+ --red-200: #ff8583;
+ --red-300: #ff6764;
+ --red-400: #fa423e;
+ --red-500: #e02e2a;
+ --red-600: #ba2623;
+ --red-700: #911e1b;
+ --red-800: #6e1615;
+ --red-900: #4d100e;
+ --red-950: #280b0a;
+ --red-1000: #1f0909;
+ --red-a25: color-mix(in oklab, var(--red-400) 8%, transparent);
+ --red-a50: color-mix(in oklab, var(--red-400) 16%, transparent);
+ --red-a75: color-mix(in oklab, var(--red-400) 30%, transparent);
+ --red-a100: color-mix(in oklab, var(--red-400) 48%, transparent);
+ --red-a200: color-mix(in oklab, var(--red-400) 64%, transparent);
+ --red-a300: color-mix(in oklab, var(--red-400) 79%, transparent);
+ --pink-25: #fff4f9;
+ --pink-50: #ffe8f3;
+ --pink-75: #ffd4e8;
+ --pink-100: #ffbada;
+ --pink-200: #ffa3ce;
+ --pink-300: #ff8cc1;
+ --pink-400: #ff66ad;
+ --pink-500: #e04c91;
+ --pink-600: #ba437a;
+ --pink-700: #963c67;
+ --pink-800: #6e2c4a;
+ --pink-900: #4d1f34;
+ --pink-950: #29101c;
+ --pink-1000: #1a0a11;
+ --pink-a25: color-mix(in oklab, var(--pink-400) 8%, transparent);
+ --pink-a50: color-mix(in oklab, var(--pink-400) 16%, transparent);
+ --pink-a75: color-mix(in oklab, var(--pink-400) 28%, transparent);
+ --pink-a100: color-mix(in oklab, var(--pink-400) 45%, transparent);
+ --pink-a200: color-mix(in oklab, var(--pink-400) 60%, transparent);
+ --pink-a300: color-mix(in oklab, var(--pink-400) 76%, transparent);
+ --orange-25: #fff5f0;
+ --orange-50: #ffe7d9;
+ --orange-75: #ffcfb4;
+ --orange-100: #ffb790;
+ --orange-200: #ff9e6c;
+ --orange-300: #ff8549;
+ --orange-400: #fb6a22;
+ --orange-500: #e25507;
+ --orange-600: #b9480d;
+ --orange-700: #923b0f;
+ --orange-800: #6d2e0f;
+ --orange-900: #4a2206;
+ --orange-950: #281105;
+ --orange-1000: #211107;
+ --orange-a25: color-mix(in oklab, var(--orange-400) 7%, transparent);
+ --orange-a50: color-mix(in oklab, var(--orange-400) 16%, transparent);
+ --orange-a75: color-mix(in oklab, var(--orange-400) 33%, transparent);
+ --orange-a100: color-mix(in oklab, var(--orange-400) 48%, transparent);
+ --orange-a200: color-mix(in oklab, var(--orange-400) 65%, transparent);
+ --orange-a300: color-mix(in oklab, var(--orange-400) 81%, transparent);
+ --yellow-25: #fffbed;
+ --yellow-50: #fff6d9;
+ --yellow-75: #ffeeb8;
+ --yellow-100: #ffe48c;
+ --yellow-200: #ffdb66;
+ --yellow-300: #ffd240;
+ --yellow-400: #ffc300;
+ --yellow-500: #e0ac00;
+ --yellow-600: #ba8e00;
+ --yellow-700: #916f00;
+ --yellow-800: #6e5400;
+ --yellow-900: #4d3b00;
+ --yellow-950: #261d00;
+ --yellow-1000: #1a1400;
+ --yellow-a25: color-mix(in oklab, var(--yellow-400) 8%, transparent);
+ --yellow-a50: color-mix(in oklab, var(--yellow-400) 15%, transparent);
+ --yellow-a75: color-mix(in oklab, var(--yellow-400) 27%, transparent);
+ --yellow-a100: color-mix(in oklab, var(--yellow-400) 45%, transparent);
+ --yellow-a200: color-mix(in oklab, var(--yellow-400) 59%, transparent);
+ --yellow-a300: color-mix(in oklab, var(--yellow-400) 74%, transparent);
+ --purple-25: #f9f5fe;
+ --purple-50: #efe5fe;
+ --purple-75: #e0cefd;
+ --purple-100: #ceb0fb;
+ --purple-200: #be95fa;
+ --purple-300: #ad7bf9;
+ --purple-400: #924ff7;
+ --purple-500: #8046d9;
+ --purple-600: #6b3ab4;
+ --purple-700: #532d8d;
+ --purple-800: #3f226a;
+ --purple-900: #2c184a;
+ --purple-950: #160c25;
+ --purple-1000: #100a19;
+ --purple-a25: color-mix(in oklab, var(--purple-400) 6%, transparent);
+ --purple-a50: color-mix(in oklab, var(--purple-400) 15%, transparent);
+ --purple-a75: color-mix(in oklab, var(--purple-400) 28%, transparent);
+ --purple-a100: color-mix(in oklab, var(--purple-400) 45%, transparent);
+ --purple-a200: color-mix(in oklab, var(--purple-400) 60%, transparent);
+ --purple-a300: color-mix(in oklab, var(--purple-400) 75%, transparent);
+ --blue-25: #f5faff;
+ --blue-50: #e5f3ff;
+ --blue-75: #cce6ff;
+ --blue-100: #99ceff;
+ --blue-200: #66b5ff;
+ --blue-300: #339cff;
+ --blue-400: #0285ff;
+ --blue-500: #0169cc;
+ --blue-600: #004f99;
+ --blue-700: #003f7a;
+ --blue-800: #013566;
+ --blue-900: #00284d;
+ --blue-950: #000e1a;
+ --blue-1000: #000d19;
+ --blue-a25: color-mix(in oklab, var(--blue-400) 4%, transparent);
+ --blue-a50: color-mix(in oklab, var(--blue-400) 13%, transparent);
+ --blue-a75: color-mix(in oklab, var(--blue-400) 25%, transparent);
+ --blue-a100: color-mix(in oklab, var(--blue-400) 40%, transparent);
+ --blue-a200: color-mix(in oklab, var(--blue-400) 60%, transparent);
+ --blue-a300: color-mix(in oklab, var(--blue-400) 80%, transparent);
+ --hairline: 1px;
+ --shadow-color: 0 0 0;
+ --elevation-100-geo: 0 1px 2px -1px;
+ --elevation-200-geo: 0 2px 4px -1px;
+ --elevation-300-geo: 0 4px 8px -2px;
+ --elevation-400-geo: 0 8px 16px -4px;
+ }
+ :where(.w-widget-scope),
+ :where([data-theme='light']) {
+ --gray-0: #fff;
+ --gray-25: #fcfcfc;
+ --gray-50: #f9f9f9;
+ --gray-75: #f3f3f3;
+ --gray-100: #ededed;
+ --gray-150: #dfdfdf;
+ --gray-200: #cdcdcd;
+ --gray-250: #b9b9b9;
+ --gray-300: #afafaf;
+ --gray-350: #9f9f9f;
+ --gray-400: #8f8f8f;
+ --gray-450: #767676;
+ --gray-550: #4f4f4f;
+ --gray-600: #414141;
+ --gray-650: #393939;
+ --gray-700: #303030;
+ --gray-750: #282828;
+ --gray-800: #212121;
+ --gray-850: #1c1c1c;
+ --gray-900: #181818;
+ --gray-925: #161616;
+ --gray-950: #131313;
+ --gray-975: #101010;
+ --gray-1000: #0d0d0d;
+ --alpha-base: #0d0d0d;
+ }
+ :where([data-theme='dark']) {
+ --gray-0: #0d0d0d;
+ --gray-25: #101010;
+ --gray-50: #131313;
+ --gray-75: #161616;
+ --gray-100: #181818;
+ --gray-150: #1c1c1c;
+ --gray-200: #212121;
+ --gray-250: #282828;
+ --gray-300: #303030;
+ --gray-350: #393939;
+ --gray-400: #414141;
+ --gray-450: #4f4f4f;
+ --gray-550: #767676;
+ --gray-600: #8f8f8f;
+ --gray-650: #9f9f9f;
+ --gray-700: #afafaf;
+ --gray-750: #b9b9b9;
+ --gray-800: #cdcdcd;
+ --gray-850: #dcdcdc;
+ --gray-900: #ededed;
+ --gray-925: #f3f3f3;
+ --gray-950: #f3f3f3;
+ --gray-975: #f9f9f9;
+ --gray-1000: #fff;
+ --alpha-base: #fff;
+ }
+ @media (resolution>=150dpi), (resolution>=1.5x) {
+ :is(.w-widget-scope, :where([data-theme])) {
+ --hairline: 0.5px;
+ }
+ }
+ :where(.w-widget-scope),
+ :where([data-theme='light']) {
+ --shadow-alpha-100: 0.08;
+ --shadow-alpha-200: 0.08;
+ --shadow-alpha-300: 0.1;
+ --shadow-alpha-400: 0.12;
+ --shadow-hairline-width: 1px;
+ --shadow-hairline-color: #00000014;
+ }
+ @media (resolution>=150dpi), (resolution>=1.5x) {
+ :is(:where(:root), :where([data-theme='light'])) {
+ --shadow-hairline-width: 0.5px;
+ --shadow-hairline-color: #0000001a;
+ }
+ }
+ :where([data-theme='dark']) {
+ --shadow-alpha-100: 0.2;
+ --shadow-alpha-200: 0.2;
+ --shadow-alpha-300: 0.36;
+ --shadow-alpha-400: 0.3;
+ --shadow-hairline-width: 1px;
+ --shadow-hairline-color: #ffffff1a;
+ }
+ @media (resolution>=150dpi), (resolution>=1.5x) {
+ :where([data-theme='dark']) {
+ --shadow-hairline-width: 0.5px;
+ --shadow-hairline-color: #ffffff1f;
+ }
+ }
+ :where([data-theme='dark']) [data-surface='elevated'] {
+ --shadow-hairline: 0 0 #0000;
+ }
+ .w-widget-scope,
+ :where([data-theme]) {
+ --color-text-emphasis: var(--gray-1000);
+ --color-text-prose: var(--color-text-emphasis);
+ --color-text-inverse: var(--gray-0);
+ --color-text-primary: var(--color-text-emphasis);
+ --color-text-primary-soft: var(--color-text-emphasis);
+ --color-background-primary-soft-alt: var(--alpha-02);
+ --color-border-primary-soft-alt: var(--alpha-06);
+ --color-text-primary-soft-alt: var(--color-text-emphasis);
+ --color-text-primary-surface: var(--color-text-emphasis);
+ --color-text-primary-solid: var(--color-text-inverse);
+ --color-text-primary-outline: var(--color-text-emphasis);
+ --color-text-primary-outline-hover: var(--color-text-emphasis);
+ --color-text-primary-ghost: var(--color-text-emphasis);
+ --color-text-primary-ghost-hover: var(--color-text-emphasis);
+ --color-ring-primary: var(--color-ring);
+ --color-ring-primary-soft: var(--color-ring-primary);
+ --color-ring-primary-solid: var(--color-ring-primary);
+ --color-ring-primary-outline: var(--color-ring-primary);
+ --color-ring-primary-ghost: var(--color-ring-primary);
+ --color-text-secondary-soft: var(--color-text);
+ --color-background-secondary-soft-alt: var(--alpha-02);
+ --color-border-secondary-soft-alt: var(--alpha-06);
+ --color-text-secondary-soft-alt: var(--color-text);
+ --color-text-secondary-solid: var(--white);
+ --color-text-secondary-outline: var(--color-text-secondary);
+ --color-text-secondary-outline-hover: var(--color-text);
+ --color-text-secondary-ghost: var(--color-text-secondary);
+ --color-text-secondary-ghost-hover: var(--color-text);
+ --color-ring-secondary: var(--color-ring);
+ --color-ring-secondary-soft: var(--color-ring-secondary);
+ --color-ring-secondary-solid: var(--color-ring-secondary);
+ --color-ring-secondary-outline: var(--color-ring-secondary);
+ --color-ring-secondary-ghost: var(--color-ring-secondary);
+ --color-background-info-soft: var(--blue-50);
+ --color-background-info-soft-hover: var(--blue-75);
+ --color-background-info-soft-active: var(--blue-75);
+ --color-background-info-soft-alpha: var(--blue-a50);
+ --color-background-info-soft-alpha-hover: var(--blue-a75);
+ --color-background-info-soft-alpha-active: var(--blue-a75);
+ --color-background-info-solid: var(--blue-400);
+ --color-background-info-solid-hover: var(--blue-500);
+ --color-background-info-solid-active: var(--blue-500);
+ --color-text-info-solid: var(--white);
+ --color-background-info-outline-hover: var(--blue-a25);
+ --color-background-info-outline-active: var(--blue-a25);
+ --color-border-info-outline: var(--blue-500);
+ --color-border-info-outline-hover: var(--blue-500);
+ --color-text-info-outline: var(--blue-500);
+ --color-text-info-outline-hover: var(--blue-500);
+ --color-background-info-ghost-hover: var(--blue-a50);
+ --color-background-info-ghost-active: var(--blue-a50);
+ --color-ring-info: var(--color-ring);
+ --color-ring-info-soft: var(--color-ring-info);
+ --color-ring-info-solid: var(--color-ring-info);
+ --color-ring-info-outline: var(--color-ring-info);
+ --color-ring-info-ghost: var(--color-ring-info);
+ --color-background-warning-soft: var(--orange-50);
+ --color-background-warning-soft-hover: var(--orange-75);
+ --color-background-warning-soft-active: var(--orange-75);
+ --color-background-warning-soft-alpha: var(--orange-a50);
+ --color-background-warning-soft-alpha-hover: var(--orange-a75);
+ --color-background-warning-soft-alpha-active: var(--orange-a75);
+ --color-background-warning-solid: var(--orange-500);
+ --color-background-warning-solid-hover: var(--orange-600);
+ --color-background-warning-solid-active: var(--orange-600);
+ --color-text-warning-solid: var(--white);
+ --color-background-warning-outline-hover: var(--orange-a25);
+ --color-background-warning-outline-active: var(--orange-a25);
+ --color-border-warning-outline: var(--orange-500);
+ --color-border-warning-outline-hover: var(--orange-500);
+ --color-text-warning-outline: var(--orange-500);
+ --color-text-warning-outline-hover: var(--orange-500);
+ --color-background-warning-ghost-hover: var(--orange-a50);
+ --color-background-warning-ghost-active: var(--orange-a50);
+ --color-text-warning-ghost: var(--orange-500);
+ --color-text-warning-ghost-hover: var(--orange-500);
+ --color-ring-warning: var(--color-ring);
+ --color-ring-warning-soft: var(--color-ring-warning);
+ --color-ring-warning-solid: var(--color-ring-warning);
+ --color-ring-warning-outline: var(--color-ring-warning);
+ --color-ring-warning-ghost: var(--color-ring-warning);
+ --color-text-caution-hover: var(--yellow-800);
+ --color-background-caution-soft: var(--yellow-50);
+ --color-background-caution-soft-hover: var(--yellow-75);
+ --color-background-caution-soft-active: var(--yellow-75);
+ --color-background-caution-soft-alpha: var(--yellow-a50);
+ --color-background-caution-soft-alpha-hover: var(--yellow-a75);
+ --color-background-caution-soft-alpha-active: var(--yellow-a75);
+ --color-background-caution-solid: var(--yellow-600);
+ --color-background-caution-solid-hover: var(--yellow-700);
+ --color-background-caution-solid-active: var(--yellow-700);
+ --color-text-caution-solid: var(--white);
+ --color-background-caution-outline-hover: var(--yellow-a25);
+ --color-background-caution-outline-active: var(--yellow-a25);
+ --color-border-caution-outline: var(--yellow-700);
+ --color-border-caution-outline-hover: var(--yellow-700);
+ --color-text-caution-outline: var(--yellow-700);
+ --color-text-caution-outline-hover: var(--yellow-700);
+ --color-background-caution-ghost-hover: var(--yellow-a50);
+ --color-background-caution-ghost-active: var(--yellow-a50);
+ --color-text-caution-ghost: var(--yellow-700);
+ --color-text-caution-ghost-hover: var(--yellow-700);
+ --color-ring-caution: var(--color-ring);
+ --color-ring-caution-soft: var(--color-ring-caution);
+ --color-ring-caution-solid: var(--color-ring-caution);
+ --color-ring-caution-outline: var(--color-ring-caution);
+ --color-ring-caution-ghost: var(--color-ring-caution);
+ --color-background-danger-soft: var(--red-50);
+ --color-background-danger-soft-hover: var(--red-75);
+ --color-background-danger-soft-active: var(--red-75);
+ --color-background-danger-soft-alpha: var(--red-a50);
+ --color-background-danger-soft-alpha-hover: var(--red-a75);
+ --color-background-danger-soft-alpha-active: var(--red-a75);
+ --color-background-danger-solid: var(--red-500);
+ --color-background-danger-solid-hover: var(--red-600);
+ --color-background-danger-solid-active: var(--red-600);
+ --color-text-danger-solid: var(--white);
+ --color-background-danger-outline-hover: var(--red-a25);
+ --color-background-danger-outline-active: var(--red-a25);
+ --color-border-danger-outline: var(--red-500);
+ --color-border-danger-outline-hover: var(--red-500);
+ --color-text-danger-outline: var(--red-500);
+ --color-text-danger-outline-hover: var(--red-500);
+ --color-background-danger-ghost-hover: var(--red-a50);
+ --color-background-danger-ghost-active: var(--red-a50);
+ --color-text-danger-ghost: var(--red-500);
+ --color-text-danger-ghost-hover: var(--red-500);
+ --color-ring-danger: var(--red-200);
+ --color-ring-danger-soft: var(--color-ring-danger);
+ --color-ring-danger-solid: var(--color-ring-danger);
+ --color-ring-danger-outline: var(--color-ring-danger);
+ --color-ring-danger-ghost: var(--color-ring-danger);
+ --color-background-success-soft: var(--green-50);
+ --color-background-success-soft-hover: var(--green-75);
+ --color-background-success-soft-active: var(--green-75);
+ --color-background-success-soft-alpha: var(--green-a50);
+ --color-background-success-soft-alpha-hover: var(--green-a75);
+ --color-background-success-soft-alpha-active: var(--green-a75);
+ --color-text-success-solid: var(--white);
+ --color-background-success-outline-hover: var(--green-a25);
+ --color-background-success-outline-active: var(--green-a25);
+ --color-text-success-outline: var(--green-500);
+ --color-text-success-outline-hover: var(--green-500);
+ --color-background-success-ghost-hover: var(--green-a50);
+ --color-background-success-ghost-active: var(--green-a50);
+ --color-text-success-ghost: var(--green-500);
+ --color-text-success-ghost-hover: var(--green-500);
+ --color-ring-success: var(--color-ring);
+ --color-ring-success-soft: var(--color-ring-info);
+ --color-ring-success-solid: var(--color-ring-info);
+ --color-ring-success-outline: var(--color-ring-info);
+ --color-ring-success-ghost: var(--color-ring-info);
+ --color-background-discovery-soft: var(--purple-50);
+ --color-background-discovery-soft-hover: var(--purple-75);
+ --color-background-discovery-soft-active: var(--purple-75);
+ --color-background-discovery-soft-alpha: var(--purple-a50);
+ --color-background-discovery-soft-alpha-hover: var(--purple-a75);
+ --color-background-discovery-soft-alpha-active: var(--purple-a75);
+ --color-background-discovery-solid: var(--purple-400);
+ --color-background-discovery-solid-hover: var(--purple-500);
+ --color-background-discovery-solid-active: var(--purple-500);
+ --color-text-discovery-solid: var(--white);
+ --color-background-discovery-outline-hover: var(--purple-a25);
+ --color-background-discovery-outline-active: var(--purple-a25);
+ --color-border-discovery-outline: var(--purple-500);
+ --color-border-discovery-outline-hover: var(--purple-500);
+ --color-background-discovery-ghost-hover: var(--purple-a50);
+ --color-background-discovery-ghost-active: var(--purple-a50);
+ --color-text-discovery-ghost: var(--purple-500);
+ --color-text-discovery-ghost-hover: var(--purple-500);
+ --color-ring-discovery: var(--color-ring);
+ --color-ring-discovery-soft: var(--color-ring);
+ --color-ring-discovery-solid: var(--color-ring);
+ --color-ring-discovery-outline: var(--color-ring);
+ --color-ring-discovery-ghost: var(--color-ring);
+ --color-background-disabled: var(--alpha-05);
+ --color-border-disabled: var(--alpha-06);
+ --font-tracking-wide: 0em;
+ --font-tracking-normal: -0.01em;
+ --font-tracking-tight: -0.02em;
+ --font-heading-5xl-size: 4.5rem;
+ --font-heading-5xl-line-height: 4.5rem;
+ --font-heading-5xl-weight: var(--font-weight-semibold);
+ --font-heading-5xl-tracking: var(--tracking-tight);
+ --font-heading-4xl-size: 3.75rem;
+ --font-heading-4xl-line-height: 3.75rem;
+ --font-heading-4xl-weight: var(--font-weight-semibold);
+ --font-heading-4xl-tracking: var(--tracking-tight);
+ --font-heading-3xl-size: 3rem;
+ --font-heading-3xl-line-height: 3rem;
+ --font-heading-3xl-weight: var(--font-weight-semibold);
+ --font-heading-3xl-tracking: var(--tracking-tight);
+ --font-heading-2xl-size: 2.25rem;
+ --font-heading-2xl-line-height: 2.625rem;
+ --font-heading-2xl-weight: var(--font-weight-semibold);
+ --font-heading-2xl-tracking: var(--tracking-tight);
+ --font-heading-xl-size: 2rem;
+ --font-heading-xl-line-height: 2.375rem;
+ --font-heading-xl-weight: var(--font-weight-semibold);
+ --font-heading-xl-tracking: var(--tracking-tight);
+ --font-heading-lg-size: 1.5rem;
+ --font-heading-lg-line-height: 1.75rem;
+ --font-heading-lg-weight: var(--font-weight-semibold);
+ --font-heading-lg-tracking: var(--tracking-normal);
+ --font-heading-md-size: 1.25rem;
+ --font-heading-md-line-height: 1.625rem;
+ --font-heading-md-weight: var(--font-weight-semibold);
+ --font-heading-md-tracking: var(--tracking-normal);
+ --font-heading-sm-size: 1.125rem;
+ --font-heading-sm-line-height: 1.625rem;
+ --font-heading-sm-weight: var(--font-weight-semibold);
+ --font-heading-sm-tracking: var(--tracking-normal);
+ --font-heading-xs-size: 1rem;
+ --font-heading-xs-line-height: 1.5rem;
+ --font-heading-xs-weight: var(--font-weight-semibold);
+ --font-heading-xs-tracking: var(--tracking-normal);
+ --font-text-xl-size: 1.25rem;
+ --font-text-xl-line-height: 1.875rem;
+ --font-text-xl-weight: var(--font-weight-normal);
+ --font-text-xl-tracking: var(--tracking-normal);
+ --font-text-lg-size: 1.125rem;
+ --font-text-lg-line-height: 1.8125rem;
+ --font-text-lg-weight: var(--font-weight-normal);
+ --font-text-lg-tracking: var(--tracking-normal);
+ --font-text-md-size: 1rem;
+ --font-text-md-line-height: 1.5rem;
+ --font-text-md-weight: var(--font-weight-normal);
+ --font-text-md-tracking: var(--tracking-normal);
+ --font-text-sm-size: 0.875rem;
+ --font-text-sm-line-height: 1.25rem;
+ --font-text-sm-weight: var(--font-weight-normal);
+ --font-text-sm-tracking: var(--tracking-normal);
+ --font-text-xs-size: 0.75rem;
+ --font-text-xs-line-height: 1.125rem;
+ --font-text-xs-weight: var(--font-weight-normal);
+ --font-text-xs-tracking: var(--tracking-wide);
+ --font-text-2xs-size: 0.625rem;
+ --font-text-2xs-line-height: 0.875rem;
+ --font-text-2xs-weight: var(--font-weight-normal);
+ --font-text-2xs-tracking: var(--tracking-wide);
+ --font-text-3xs-size: 0.5rem;
+ --font-text-3xs-line-height: 0.75rem;
+ --font-text-3xs-weight: var(--font-weight-normal);
+ --font-text-3xs-tracking: var(--tracking-wide);
+ --font-small-caps-md-size: 0.6875rem;
+ --font-small-caps-md-line-height: 0.9375rem;
+ --font-small-caps-md-weight: var(--font-weight-semibold);
+ --font-small-caps-md-tracking: 0.65px;
+ --font-small-caps-lg-size: 0.8125rem;
+ --font-small-caps-lg-line-height: 1.1875rem;
+ --font-small-caps-lg-weight: var(--font-weight-semibold);
+ --font-small-caps-lg-tracking: 0.6px;
+ --control-size-3xs: 1.375rem;
+ --control-size-2xs: 1.5rem;
+ --control-size-xs: 1.625rem;
+ --control-size-sm: 1.75rem;
+ --control-size-md: 2rem;
+ --control-size-lg: 2.25rem;
+ --control-size-xl: 2.5rem;
+ --control-size-2xl: 2.75rem;
+ --control-size-3xl: 3rem;
+ --control-gutter-2xs: 0.375rem;
+ --control-gutter-xs: 0.5rem;
+ --control-gutter-sm: 0.625rem;
+ --control-gutter-md: 0.75rem;
+ --control-gutter-lg: 0.875rem;
+ --control-gutter-xl: 1rem;
+ --control-gutter-pill-scaling: 1.33;
+ --control-radius-sm: var(--radius-sm);
+ --control-radius-md: var(--radius-md);
+ --control-radius-lg: var(--radius-lg);
+ --control-radius-xl: var(--radius-xl);
+ --control-font-size-sm: var(--font-text-xs-size);
+ --control-font-size-md: var(--font-text-sm-size);
+ --control-font-size-lg: var(--font-text-md-size);
+ --control-icon-size-xs: 0.875rem;
+ --control-icon-size-sm: 1rem;
+ --control-icon-size-md: 1.125rem;
+ --control-icon-size-lg: 1.25rem;
+ --control-icon-size-xl: 1.375rem;
+ --control-icon-size-2xl: 1.5rem;
+ --cubic-enter: cubic-bezier(0.19, 1, 0.22, 1);
+ --cubic-exit: cubic-bezier(0.8, 0, 0.4, 1);
+ --cubic-exit-snappy: cubic-bezier(0.65, 0, 0.4, 1);
+ --cubic-move: cubic-bezier(0.65, 0, 0.35, 1);
+ --transition-duration-basic: 0.15s;
+ --transition-ease-basic: ease;
+ --scrollbar-color: var(--alpha-30);
+ --shadow-hairline: 0 0 0 var(--shadow-hairline-width) var(--shadow-hairline-color);
+ --shadow-100: var(--elevation-100-geo) rgb(var(--shadow-color) / var(--shadow-alpha-100));
+ --shadow-100-strong: var(--elevation-100-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-100) * 1.25));
+ --shadow-100-stronger: var(--elevation-100-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-100) * 1.6));
+ --shadow-200: var(--elevation-200-geo) rgb(var(--shadow-color) / var(--shadow-alpha-200));
+ --shadow-200-strong: var(--elevation-200-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-200) * 1.25));
+ --shadow-200-stronger: var(--elevation-200-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-200) * 1.6));
+ --shadow-300: var(--elevation-300-geo) rgb(var(--shadow-color) / var(--shadow-alpha-300));
+ --shadow-300-strong: var(--elevation-300-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-300) * 1.25));
+ --shadow-300-stronger: var(--elevation-300-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-300) * 1.6));
+ --shadow-400: var(--elevation-400-geo) rgb(var(--shadow-color) / var(--shadow-alpha-400));
+ --shadow-400-strong: var(--elevation-400-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-400) * 1.25));
+ --shadow-400-stronger: var(--elevation-400-geo) rgb(var(--shadow-color) / calc(var(--shadow-alpha-400) * 1.6));
+ --alert-border-radius: var(--radius-xl);
+ --alert-gap: calc(var(--spacing) * 3);
+ --alert-gutter: calc(var(--spacing) * 4);
+ --alert-font-size: var(--font-text-sm-size);
+ --alert-line-height: var(--font-text-sm-line-height);
+ --alert-title-font-weight: var(--font-weight-semibold);
+ --avatar-radius: var(--radius-full);
+ --avatar-size: 28px;
+ --avatar-font-size-scaling: 0.5;
+ --avatar-overflow-font-size-scaling-one: 0.45;
+ --avatar-overflow-font-size-scaling-two: 0.37;
+ --avatar-overflow-font-size-scaling-three: 0.3;
+ --avatar-group-cutout-width: 3px;
+ --avatar-group-cutout-color: var(--color-surface);
+ --avatar-group-spacing: -8px;
+ --badge-gutter-sm: calc(var(--control-gutter-2xs) - 1px);
+ --badge-gutter-md: var(--control-gutter-2xs);
+ --badge-gutter-lg: var(--control-gutter-xs);
+ --badge-size-sm: calc(var(--control-size-3xs) - 2px);
+ --badge-size-md: var(--control-size-3xs);
+ --badge-size-lg: var(--control-size-2xs);
+ --badge-radius-sm: var(--radius-xs);
+ --badge-radius-md: var(--radius-xs);
+ --badge-radius-lg: var(--radius-sm);
+ --badge-font-size-sm: var(--font-text-xs-size);
+ --badge-font-size-md: var(--font-text-sm-size);
+ --badge-font-size-lg: var(--font-text-sm-size);
+ --badge-tracking-sm: var(--tracking-wide);
+ --badge-tracking-md: var(--tracking-normal);
+ --badge-tracking-lg: var(--tracking-normal);
+ --badge-font-weight-sm: var(--font-weight-semibold);
+ --badge-font-weight-md: var(--font-weight-semibold);
+ --badge-font-weight-lg: var(--font-weight-semibold);
+ --badge-icon-font-size-sm: var(--font-text-xs-size);
+ --badge-icon-font-size-md: var(--font-text-md-size);
+ --badge-icon-font-size-lg: var(--font-text-md-size);
+ --badge-indicator-size-sm: var(--font-text-xs-size);
+ --badge-indicator-size-md: var(--font-text-xs-size);
+ --badge-indicator-size-lg: var(--font-text-sm-size);
+ --button-gap-sm: 3px;
+ --button-gap-md: 4px;
+ --button-gap-lg: 6px;
+ --button-font-weight: var(--font-weight-medium);
+ --input-gap-xs: 4px;
+ --input-gap-sm: 6px;
+ --input-gap-md: 8px;
+ --input-gap-lg: 10px;
+ --input-text-color: var(--color-text);
+ --input-placeholder-text-color: var(--color-text-tertiary);
+ --input-outline-border-color: var(--color-border-primary-outline);
+ --input-outline-border-color-focus: var(--alpha-50);
+ --input-soft-background-color: var(--color-background-primary-soft-alpha);
+ --input-soft-border-color-focus: var(--alpha-20);
+ --link-font-weight: inherit;
+ --link-gap: calc(var(--spacing) * 0.5);
+ --link-radius: var(--radius-sm);
+ --link-underline-decoration-offset: 0.1em;
+ --chat-max-width: 800px;
+ --chat-gutter: calc(var(--spacing) * 5);
+ --chat-background-color: var(--color-surface);
+ --thread-gutter: calc(var(--spacing) * 4);
+ --composer-gutter: calc(var(--spacing) * 3);
+ --composer-compact-gutter: calc(var(--spacing) * 2);
+ --composer-radius: var(--radius-4xl);
+ --composer-background-color: var(--color-surface-elevated);
+ --smoothing-background-color: var(--color-surface);
+ --user-message-text-color: var(--color-text-prose);
+ --source-list-gutter: var(--thread-gutter);
+ --codeblock-background-color: var(--gray-25);
+ --codeblock-syntax-4: var(--pink-500);
+ --dialog-min-width: 250px;
+ --dialog-max-width: 450px;
+ --dialog-container-inner-padding: calc(var(--spacing) * 5);
+ --dialog-backdrop-fade-background: color-mix(in oklab, var(--color-surface-elevated) 60%, transparent);
+ --menu-gutter: calc(var(--spacing) * 1.5);
+ --menu-radius: var(--radius-xl);
+ --menu-font-size: var(--font-text-sm-size);
+ --menu-line-height: var(--font-text-sm-line-height);
+ --menu-item-padding: calc(var(--spacing) * 1.5) calc(var(--spacing) * 2);
+ --menu-item-gap: calc(var(--spacing) * 1.5);
+ --menu-separator-gutter: var(--menu-gutter) calc(-1 * var(--menu-gutter));
+ --menu-separator-background-color: var(--color-border);
+ --menu-radio-indicator-size: var(--font-text-lg-size);
+ --menu-radio-indicator-hole-size: var(--font-text-3xs-size);
+ --menu-checkbox-indicator-size: var(--font-text-lg-size);
+ --modal-container-inner-padding: calc(var(--spacing) * 5);
+ --popover-radius: var(--radius-xl);
+ --radio-group-col-gap: calc(var(--spacing) * 2.5);
+ --radio-group-row-gap: calc(var(--spacing) * 5);
+ --radio-group-item-gap: calc(var(--spacing) * 1.5);
+ --radio-group-item-font-size: var(--font-text-sm-size);
+ --radio-group-item-line-height: var(--font-text-sm-line-height);
+ --radio-group-indicator-size: var(--font-text-md-size);
+ --radio-group-indicator-border-color: var(--color-border-primary-outline);
+ --radio-group-indicator-border-color-hover: var(--alpha-25);
+ --radio-group-indicator-background-color: var(--color-background-primary-solid);
+ --radio-group-indicator-hole-size: 0.375rem;
+ --radio-group-indicator-hole-background-color: var(--color-text-primary-solid);
+ --segmented-control-gap: 2px;
+ --segmented-control-gutter: 2px;
+ --segmented-control-font-weight: var(--font-weight-semibold);
+ --segmented-control-thumb-shadow: 0 1px 4px -1px #0003;
+ --segmented-control-option-highlight-gutter: 1px;
+ --select-control-font-weight: var(--font-weight-medium);
+ --switch-track-width: 32px;
+ --switch-track-height: 19px;
+ --switch-thumb-offset: 3px;
+ --switch-thumb-size: calc(var(--switch-track-height) - 2 * var(--switch-thumb-offset));
+ --switch-thumb-shadow: 0 1px 2px #0003;
+ --switch-label-gap: calc(var(--spacing) * 2);
+ }
+ :where(.w-widget-scope),
+ :where([data-theme='light']) {
+ --color-text: var(--gray-750);
+ --color-text-secondary: var(--gray-500);
+ --color-text-tertiary: var(--gray-400);
+ --color-ring: var(--blue-500);
+ --color-background-primary-soft: var(--gray-100);
+ --color-background-primary-soft-hover: var(--gray-150);
+ --color-background-primary-soft-active: var(--gray-200);
+ --color-background-primary-soft-alpha: var(--alpha-08);
+ --color-background-primary-soft-alpha-hover: var(--alpha-12);
+ --color-background-primary-soft-alpha-active: var(--alpha-16);
+ --color-background-primary-surface: var(--alpha-05);
+ --color-border-primary-surface: var(--alpha-05);
+ --color-background-primary-solid: var(--gray-900);
+ --color-background-primary-solid-hover: var(--gray-700);
+ --color-background-primary-solid-active: var(--gray-600);
+ --color-background-primary-outline-hover: var(--alpha-02);
+ --color-background-primary-outline-active: var(--alpha-04);
+ --color-border-primary-outline: var(--alpha-16);
+ --color-border-primary-outline-hover: var(--alpha-20);
+ --color-background-primary-ghost-hover: var(--alpha-08);
+ --color-background-primary-ghost-active: var(--alpha-12);
+ --color-background-secondary-soft: var(--gray-100);
+ --color-background-secondary-soft-hover: var(--gray-150);
+ --color-background-secondary-soft-active: var(--gray-200);
+ --color-background-secondary-soft-alpha: var(--alpha-08);
+ --color-background-secondary-soft-alpha-hover: var(--alpha-12);
+ --color-background-secondary-soft-alpha-active: var(--alpha-16);
+ --color-background-secondary-solid: var(--gray-500);
+ --color-background-secondary-solid-hover: var(--gray-600);
+ --color-background-secondary-solid-active: var(--gray-700);
+ --color-background-secondary-outline-hover: var(--alpha-02);
+ --color-background-secondary-outline-active: var(--alpha-04);
+ --color-border-secondary-outline: var(--alpha-16);
+ --color-border-secondary-outline-hover: var(--alpha-20);
+ --color-background-secondary-ghost-hover: var(--alpha-08);
+ --color-background-secondary-ghost-active: var(--alpha-12);
+ --color-text-info: var(--blue-500);
+ --color-text-info-soft: var(--blue-600);
+ --color-background-info-surface: var(--blue-a25);
+ --color-border-info-surface: var(--blue-a25);
+ --color-text-info-surface: var(--blue-600);
+ --color-text-info-ghost: var(--blue-500);
+ --color-text-info-ghost-hover: var(--blue-500);
+ --color-text-warning: var(--orange-700);
+ --color-text-warning-soft: var(--orange-700);
+ --color-background-warning-surface: var(--orange-a25);
+ --color-border-warning-surface: var(--orange-a25);
+ --color-text-warning-surface: var(--orange-700);
+ --color-text-caution: var(--yellow-700);
+ --color-text-caution-soft: var(--yellow-800);
+ --color-background-caution-surface: var(--yellow-a25);
+ --color-border-caution-surface: var(--yellow-a25);
+ --color-text-caution-surface: var(--yellow-800);
+ --color-text-danger: var(--red-700);
+ --color-text-danger-soft: var(--red-600);
+ --color-background-danger-surface: var(--red-a25);
+ --color-border-danger-surface: var(--red-a25);
+ --color-text-danger-surface: var(--red-600);
+ --color-text-success: var(--green-700);
+ --color-text-success-soft: var(--green-600);
+ --color-background-success-surface: var(--green-a25);
+ --color-border-success-surface: var(--green-a25);
+ --color-text-success-surface: var(--green-600);
+ --color-background-success-solid: var(--green-500);
+ --color-background-success-solid-hover: var(--green-500);
+ --color-background-success-solid-active: var(--green-500);
+ --color-border-success-outline: var(--green-500);
+ --color-border-success-outline-hover: var(--green-500);
+ --color-text-discovery: var(--purple-700);
+ --color-text-discovery-soft: var(--purple-600);
+ --color-background-discovery-surface: var(--purple-a25);
+ --color-border-discovery-surface: var(--purple-a25);
+ --color-text-discovery-surface: var(--purple-600);
+ --color-text-discovery-outline: var(--purple-500);
+ --color-text-discovery-outline-hover: var(--purple-500);
+ --color-text-disabled: var(--gray-400);
+ --color-border-subtle: var(--alpha-05);
+ --color-border: var(--alpha-10);
+ --color-border-strong: var(--alpha-15);
+ --shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
+ --color-surface: var(--gray-0);
+ --color-surface-secondary: var(--gray-50);
+ --color-surface-tertiary: var(--gray-75);
+ --color-surface-elevated: var(--gray-0);
+ --color-surface-elevated-secondary: var(--gray-50);
+ --avatar-image-border-color: var(--alpha-04);
+ --input-outline-border-color-hover: var(--alpha-25);
+ --input-border-color-invalid: var(--red-500);
+ --link-primary-text-color: var(--blue-500);
+ --link-primary-text-color-hover: var(--blue-800);
+ --user-message-background-color: var(--alpha-05);
+ --codeblock-syntax-1: #c0660d;
+ --codeblock-syntax-2: var(--blue-500);
+ --codeblock-syntax-3: var(--green-600);
+ --codeblock-syntax-5: var(--purple-500);
+ --dialog-backdrop-dim-background: #0000004d;
+ --menu-item-background-color: var(--alpha-08);
+ --modal-backdrop-background: #0000004d;
+ --segmented-control-background: var(--gray-100);
+ --segmented-control-thumb-background: var(--gray-0);
+ --segmented-control-option-highlight-background-color: var(--gray-200);
+ --slider-track-color: var(--gray-150);
+ --slider-range-color: var(--gray-450);
+ --switch-track-color: var(--gray-150);
+ --switch-track-color-hover: var(--gray-200);
+ --switch-track-color-checked: var(--gray-900);
+ --switch-track-color-checked-disabled: var(--gray-300);
+ --switch-track-color-disabled: var(--gray-100);
+ --switch-thumb-color: var(--gray-0);
+ --switch-thumb-color-disabled: var(--gray-0);
+ }
+ :where([data-theme='dark']) {
+ --color-text: var(--gray-850);
+ --color-text-secondary: var(--gray-700);
+ --color-text-tertiary: var(--gray-600);
+ --color-ring: var(--blue-400);
+ --color-background-primary-soft: var(--gray-300);
+ --color-background-primary-soft-hover: var(--gray-350);
+ --color-background-primary-soft-active: var(--gray-400);
+ --color-background-primary-soft-alpha: var(--alpha-12);
+ --color-background-primary-soft-alpha-hover: var(--alpha-16);
+ --color-background-primary-soft-alpha-active: var(--alpha-20);
+ --color-background-primary-surface: var(--alpha-08);
+ --color-border-primary-surface: var(--alpha-08);
+ --color-background-primary-solid: var(--gray-950);
+ --color-background-primary-solid-hover: var(--gray-900);
+ --color-background-primary-solid-active: var(--gray-850);
+ --color-background-primary-outline-hover: var(--alpha-04);
+ --color-background-primary-outline-active: var(--alpha-06);
+ --color-border-primary-outline: var(--alpha-25);
+ --color-border-primary-outline-hover: var(--alpha-30);
+ --color-background-primary-ghost-hover: var(--alpha-12);
+ --color-background-primary-ghost-active: var(--alpha-16);
+ --color-background-secondary-soft: var(--gray-300);
+ --color-background-secondary-soft-hover: var(--gray-350);
+ --color-background-secondary-soft-active: var(--gray-400);
+ --color-background-secondary-soft-alpha: var(--alpha-12);
+ --color-background-secondary-soft-alpha-hover: var(--alpha-16);
+ --color-background-secondary-soft-alpha-active: var(--alpha-20);
+ --color-background-secondary-solid: var(--gray-400);
+ --color-background-secondary-solid-hover: var(--gray-450);
+ --color-background-secondary-solid-active: var(--gray-500);
+ --color-background-secondary-outline-hover: var(--alpha-04);
+ --color-background-secondary-outline-active: var(--alpha-06);
+ --color-border-secondary-outline: var(--alpha-25);
+ --color-border-secondary-outline-hover: var(--alpha-30);
+ --color-background-secondary-ghost-hover: var(--alpha-12);
+ --color-background-secondary-ghost-active: var(--alpha-16);
+ --color-text-info: var(--blue-200);
+ --color-text-info-soft: var(--blue-300);
+ --color-background-info-surface: var(--blue-a50);
+ --color-border-info-surface: var(--blue-a50);
+ --color-text-info-surface: var(--blue-300);
+ --color-text-info-ghost: var(--blue-200);
+ --color-text-info-ghost-hover: var(--blue-200);
+ --color-text-warning: var(--orange-500);
+ --color-text-warning-soft: var(--orange-400);
+ --color-background-warning-surface: var(--orange-a50);
+ --color-border-warning-surface: var(--orange-a50);
+ --color-text-warning-surface: var(--orange-400);
+ --color-text-caution: var(--yellow-500);
+ --color-text-caution-soft: var(--yellow-400);
+ --color-background-caution-surface: var(--yellow-a50);
+ --color-border-caution-surface: var(--yellow-a50);
+ --color-text-caution-surface: var(--yellow-400);
+ --color-text-danger: var(--red-500);
+ --color-text-danger-soft: var(--red-400);
+ --color-background-danger-surface: var(--red-a50);
+ --color-border-danger-surface: var(--red-a50);
+ --color-text-danger-surface: var(--red-400);
+ --color-text-success: var(--green-400);
+ --color-text-success-soft: var(--green-400);
+ --color-background-success-surface: var(--green-a50);
+ --color-border-success-surface: var(--green-a50);
+ --color-text-success-surface: var(--green-400);
+ --color-background-success-solid: var(--green-600);
+ --color-background-success-solid-hover: var(--green-600);
+ --color-background-success-solid-active: var(--green-600);
+ --color-border-success-outline: var(--green-600);
+ --color-border-success-outline-hover: var(--green-600);
+ --color-text-discovery: var(--purple-500);
+ --color-text-discovery-soft: var(--purple-200);
+ --color-background-discovery-surface: var(--purple-a50);
+ --color-border-discovery-surface: var(--purple-a50);
+ --color-text-discovery-surface: var(--purple-200);
+ --color-text-discovery-outline: var(--purple-400);
+ --color-text-discovery-outline-hover: var(--purple-400);
+ --color-text-disabled: var(--gray-500);
+ --color-border-subtle: var(--alpha-06);
+ --color-border: var(--alpha-12);
+ --color-border-strong: var(--alpha-20);
+ --shadow: 0 10px 15px -3px #0003, 0 4px 6px -4px #0003;
+ --color-surface: var(--gray-200);
+ --color-surface-secondary: var(--gray-100);
+ --color-surface-tertiary: var(--gray-50);
+ --color-surface-elevated: var(--gray-300);
+ --color-surface-elevated-secondary: var(--gray-400);
+ --avatar-image-border-color: var(--alpha-15);
+ --input-outline-border-color-hover: var(--alpha-30);
+ --input-border-color-invalid: var(--red-600);
+ --link-primary-text-color: var(--blue-300);
+ --link-primary-text-color-hover: var(--blue-400);
+ --user-message-background-color: var(--alpha-08);
+ --codeblock-syntax-1: var(--yellow-100);
+ --codeblock-syntax-2: var(--blue-200);
+ --codeblock-syntax-3: var(--green-300);
+ --codeblock-syntax-5: var(--purple-300);
+ --dialog-backdrop-dim-background: #00000080;
+ --menu-item-background-color: var(--alpha-10);
+ --modal-backdrop-background: #00000080;
+ --segmented-control-background: var(--gray-0);
+ --segmented-control-thumb-background: var(--gray-300);
+ --segmented-control-option-highlight-background-color: var(--gray-300);
+ --slider-track-color: var(--gray-400);
+ --slider-range-color: var(--gray-600);
+ --switch-track-color: var(--gray-400);
+ --switch-track-color-hover: var(--gray-450);
+ --switch-track-color-checked: var(--blue-400);
+ --switch-track-color-checked-disabled: var(--blue-700);
+ --switch-track-color-disabled: var(--gray-300);
+ --switch-thumb-color: var(--gray-1000);
+ --switch-thumb-color-disabled: var(--gray-800);
+ }
+}
+
+/* Box Component */
+.w-box {
+ display: flex;
+ box-sizing: border-box;
+}
+
+/* Text Component */
+.w-text {
+ display: inline;
+ box-sizing: border-box;
+}
+
+/* Title Component */
+.w-title {
+ display: block;
+ box-sizing: border-box;
+ margin: 0;
+}
+
+/* Select Component */
+.w-select {
+ display: inline-flex;
+ box-sizing: border-box;
+ position: relative;
+ align-items: center;
+}
+
+.w-select.w-full {
+ display: flex;
+ width: 100%;
+}
+
+.w-select .w-full {
+ width: 100%;
+}
+
+.w-select[data-w-component='select'] {
+ gap: 0.5rem;
+}
+
+.w-select.select-pill [data-slot='select-trigger'] {
+ border-radius: var(--radius-full);
+}
+
+.w-select.select-disabled {
+ opacity: 0.6;
+ pointer-events: none;
+}
+
+.w-select.select-disabled [data-slot='select-trigger'] {
+ background-color: var(--color-surface-secondary);
+ cursor: not-allowed;
+}
+
+/* Checkbox Component */
+.w-checkbox {
+ display: inline-flex;
+ box-sizing: border-box;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.w-checkbox label {
+ user-select: none;
+ cursor: pointer;
+}
+
+.w-checkbox[data-disabled='true'] label {
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+/* RadioGroup Component */
+.w-radio-group {
+ display: flex;
+ box-sizing: border-box;
+ gap: 0.75rem;
+}
+
+.w-radio-group label {
+ user-select: none;
+ cursor: pointer;
+}
+
+.w-radio-group[data-disabled='true'] label {
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+.w-radio-group[direction='col'] {
+ flex-direction: column;
+}
+
+.w-radio-group[direction='row'] {
+ flex-direction: row;
+}
+
+/* Textarea Component */
+.w-textarea {
+ display: inline-flex;
+ box-sizing: border-box;
+ position: relative;
+ width: 100%;
+}
+
+.w-textarea textarea {
+ /* Styled by shadcn textarea */
+ resize: vertical;
+}
+
+.w-textarea[data-disabled='true'] textarea {
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+/* Form Component */
+.w-form {
+ display: flex;
+ box-sizing: border-box;
+ position: relative;
+}
+
+/* Form element styling */
+/* Spacer Component */
+.w-spacer {
+ display: flex;
+ box-sizing: border-box;
+ flex: 1 1 auto;
+ min-width: var(--w-spacer-min-size, auto);
+ min-height: var(--w-spacer-min-size, auto);
+}
+
+/* Divider Component */
+.w-divider {
+ width: 100%;
+ height: var(--divider-size, 1px);
+ background-color: var(--divider-color, var(--color-border));
+ border: none;
+ /* Default spacing: auto spacing based on siblings */
+ margin-block-start: calc(var(--spacing) * 4);
+}
+
+.w-divider[role='separator'] {
+ display: flex;
+}
+
+/* When manual spacing is set, use it instead of auto spacing */
+.w-divider[data-w-manual-spacing] {
+ margin-block-start: 0;
+}
+
+/* Next sibling with data-w-component should have margin when after divider */
+[data-w-component='divider'] + [data-w-component] {
+ margin-block-start: calc(var(--spacing) * 4);
+}
+
+/* Unless divider has manual spacing, then clear the margin */
+[data-w-component='divider'][data-w-manual-spacing] + [data-w-component] {
+ margin-block-start: 0 !important;
+}
+
+.w-divider.divider-flush {
+ margin-left: 0;
+ margin-right: 0;
+}
+
+/* Caption Component */
+.w-caption {
+ display: inline;
+ box-sizing: border-box;
+}
+
+.w-caption[data-w-component='caption'][data-w-size='sm'] {
+ font-size: var(--font-text-2xs-size);
+ line-height: 1.3;
+ letter-spacing: var(--font-text-2xs-tracking);
+}
+
+.w-caption[data-w-component='caption'][data-w-size='md'] {
+ font-size: var(--font-text-xs-size);
+ line-height: 1.3;
+ letter-spacing: var(--font-text-xs-tracking);
+}
+
+.w-caption[data-w-component='caption'][data-w-size='lg'] {
+ font-size: var(--font-text-sm-size);
+ line-height: 1.3;
+ letter-spacing: var(--font-text-sm-tracking);
+}
+
+/* Label Component */
+.w-label {
+ display: inline-block;
+ box-sizing: border-box;
+ cursor: pointer;
+}
+
+/* Markdown Component */
+.w-markdown {
+ display: block;
+ box-sizing: border-box;
+}
+
+.w-markdown[data-w-component='markdown'] {
+ width: 100%;
+}
+
+.w-markdown.markdown-streaming {
+ /* Streaming-friendly transitions */
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0.8;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* Icon Component */
+.w-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ box-sizing: border-box;
+}
+
+/* Chart Component */
+.w-chart {
+ display: block;
+ box-sizing: border-box;
+ position: relative;
+}
+
+.w-chart[data-w-component='chart'] {
+ width: 100%;
+ min-height: 240px;
+}
+
+/* Badge Component */
+.w-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 500;
+ transition: colors 0.2s ease-in-out;
+ box-sizing: border-box;
+}
+
+/* Badge Sizes */
+.badge-sm {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ height: 1.25rem;
+}
+
+.badge-md {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ height: 1.5rem;
+}
+
+.badge-lg {
+ padding: 0.5rem 1rem;
+ font-size: 1rem;
+ height: 1.75rem;
+}
+
+/* Badge Pill Shape */
+.badge-pill {
+ border-radius: 9999px;
+}
+
+/* Badge Colors - Soft Variant (Default) */
+.badge-soft {
+ border: 1px solid transparent;
+}
+
+.badge-info.badge-soft {
+ background-color: var(--color-bg-info-soft);
+ color: var(--color-text-info);
+}
+
+.badge-secondary.badge-soft {
+ background-color: var(--color-bg-secondary-soft);
+ color: var(--color-text-secondary);
+}
+
+.badge-discovery.badge-soft {
+ background-color: var(--color-bg-discovery-soft);
+ color: var(--color-text-discovery);
+}
+
+.badge-success.badge-soft {
+ background-color: var(--color-bg-success-soft);
+ color: var(--color-text-success);
+}
+
+.badge-warning.badge-soft {
+ background-color: var(--color-bg-warning-soft);
+ color: var(--color-text-warning);
+}
+
+.badge-danger.badge-soft {
+ background-color: var(--color-bg-danger-soft);
+ color: var(--color-text-danger);
+}
+
+/* Badge Colors - Solid Variant */
+.badge-solid {
+ border: 1px solid transparent;
+ color: white;
+}
+
+.badge-info.badge-solid {
+ background-color: var(--color-bg-info);
+}
+
+.badge-secondary.badge-solid {
+ background-color: var(--color-bg-secondary);
+}
+
+.badge-discovery.badge-solid {
+ background-color: var(--color-bg-discovery);
+}
+
+.badge-success.badge-solid {
+ background-color: var(--color-bg-success);
+}
+
+.badge-warning.badge-solid {
+ background-color: var(--color-bg-warning);
+}
+
+.badge-danger.badge-solid {
+ background-color: var(--color-bg-danger);
+}
+
+/* Badge Colors - Outline Variant */
+.badge-outline {
+ background-color: transparent;
+ border: 1px solid;
+}
+
+.badge-info.badge-outline {
+ border-color: var(--color-border-info);
+ color: var(--color-text-info);
+}
+
+.badge-secondary.badge-outline {
+ border-color: var(--color-border-secondary);
+ color: var(--color-text-secondary);
+}
+
+.badge-discovery.badge-outline {
+ border-color: var(--color-border-discovery);
+ color: var(--color-text-discovery);
+}
+
+.badge-success.badge-outline {
+ border-color: var(--color-border-success);
+ color: var(--color-text-success);
+}
+
+.badge-warning.badge-outline {
+ border-color: var(--color-border-warning);
+ color: var(--color-text-warning);
+}
+
+.badge-danger.badge-outline {
+ border-color: var(--color-border-danger);
+ color: var(--color-text-danger);
+}
+
+/* Reset styles */
+.w-card *,
+.w-box *,
+.w-text *,
+.w-title *,
+.w-icon,
+.w-chart,
+.w-badge,
+.w-datepicker,
+.w-select,
+.w-checkbox,
+.w-radio-group,
+.w-input,
+.w-textarea,
+.w-form,
+.w-spacer,
+.w-divider,
+.w-caption,
+.w-label,
+.w-markdown {
+ box-sizing: border-box;
+}
diff --git a/web/src/config/en.json b/web/src/config/en.json
index 7b04e12..24bb849 100644
--- a/web/src/config/en.json
+++ b/web/src/config/en.json
@@ -15,6 +15,7 @@
"workspace.controller.real-time": "Real-Time",
"workspace.controller.jump-to-real-time": "Jump to Real-Time",
"workspace.task.progress": "Task Progress",
+ "workspace.task.current_progress": "Progress ",
"sender.placeholder": "Please give me a task",
"sender.placeholder.progress": "Task in progress",
"sender.knowledge-base": "Knowledge Base",
@@ -27,9 +28,11 @@
"sender.mcp.selected.sandbox": "Selected Sandbox",
"sender.mcp.selected.plugins": "Selected Plugins",
"sender.mcp.selected.workflow": "Sandbox Workflow",
+ "sender.mcp.selected.skills": "Selected Skills",
"sender.replay.restart": "Restart",
"sender.replay.end": "Jump to End",
"button.ok": "OK",
+ "button.save": "Save",
"button.confirm": "Confirm",
"button.cancel": "Cancel",
"share.password": "Password",
@@ -41,6 +44,11 @@
"file.upload.count.error": "The number of files cannot exceed",
"file.upload.attachments": "Attachments",
"chatbot.task.thinking": "Thinking",
+ "chatbot.task.reconnect.failed": "Connection failed after {{count}} retries",
+ "chatbot.task.network.offline": "Network disconnected. Will automatically resume when network is restored...",
+ "chatbot.task.network.resuming": "Network restored, resuming task...",
+ "chatbot.task.server.reconnecting": "Server connection lost, reconnecting... ({{current}}/{{max}})",
+ "chatbot.task.connection.interrupted": "Connection interrupted, attempting to recover...",
"code.diff": "Diff",
"code.old": "Old",
"code.new": "New",
@@ -55,6 +63,7 @@
"tool.tab.ai-app": "AI Applications",
"tool.tab.plugins": "Plugins",
"tool.tab.workflow": "Workflow",
+ "tool.tab.skills": "Skills",
"task.finish.reason.completed": "Completed Current Task",
"task.finish.reason.failed": "Task Failed",
"task.finish.reason.cancelled": "User Cancelled",
@@ -93,6 +102,128 @@
"form.submit.loading": "Submitting...",
"form.submit.button": "Submit",
"form.error.checkInput": "Please check form input",
+ "user_input.form.submitted.title": "Submitted {{title}}",
+ "user_input.form.submitted.default_title": "Submitted form information",
+ "user_input.form.submitted.field_header": "Field",
+ "user_input.form.submitted.value_header": "Value",
+ "user_input.form.submitted.array_separator": ", ",
+ "user_input.form.submitted.empty_value": "-",
+ "user_input.text.placeholder": "Enter your reply",
+ "user_input.custom.placeholder": "Other, please enter...",
+ "hitl.decision.approve": "approve",
+ "hitl.decision.reject": "reject",
+ "user_input.approval.custom.placeholder": "Enter rejection reason",
+ "user_input.none_selected": "No options selected",
"inputoutput.input.label": "Input",
- "inputoutput.output.label": "Output"
+ "inputoutput.output.label": "Output",
+ "error.image.parser.failed": "Image parsing failed",
+ "agent.default.greeting": "What can I help with today?",
+ "attachment.download": "Download",
+ "sender.stop": "Stop",
+ "reference.default_title": "Referenced Content",
+ "reference.draft_label": "Referenced",
+ "reference.remove": "Remove reference",
+ "error.generic": "An error occurred",
+ "error.unknown": "Unknown error",
+ "error.image.url_missing": "Image URL is missing",
+ "files.fetch.failed": "Failed to fetch files",
+ "link.copied": "Link copied",
+ "link.copy_failed": "Copy failed",
+ "website_delivery.failed": "Website delivery failed",
+ "website_delivery.default_name": "AI Website",
+ "website_delivery.no_files": "No files",
+ "website_delivery.no_file_selected": "No file selected",
+ "website_delivery.no_preview_url": "No preview URL",
+ "service_deploy.failed": "Service deploy failed",
+ "service_deploy.default_name": "Web App",
+ "action.refresh": "Refresh",
+ "action.share": "Share",
+ "workspace.open": "Open workspace",
+ "workspace.collapse_task_progress": "Collapse task progress",
+ "workspace.expand_task_progress": "Expand task progress",
+ "share.fetch_failed": "Failed to fetch share details",
+ "welcome.title": "Welcome to LangCrew",
+ "welcome.description": "Provide the agent with a mission.",
+ "home.welcome.title": "Welcome to Wanzhi Super Employees",
+ "home.welcome.desc": "Choose a professional super employee to provide customized intelligent solutions for your industry",
+ "home.apply": "Apply for Access",
+ "home.cases.title": "Great Cases from Wanzhi Customers",
+ "home.core_capability": "Core Capabilities",
+ "home.coming_soon": "More super employees are in production. Stay tuned!",
+ "fallback.404.title": "404",
+ "fallback.404.subtitle": "Sorry, the page you visited does not exist.",
+ "cloudphone.sdk.load_failed": "SDK failed to load",
+ "cloudphone.sdk.not_found": "SDK not found",
+ "request.failed": "Request failed",
+ "request.network_error": "Network error",
+ "request.bad_request": "Invalid request parameters",
+ "request.unauthorized": "Unauthorized, please log in again",
+ "request.forbidden": "Access denied",
+ "request.not_found": "Requested resource does not exist",
+ "request.server_error": "Internal server error",
+ "request.bad_gateway": "Bad gateway",
+ "request.service_unavailable": "Service unavailable",
+ "request.failed_with_status": "Request failed ({{status}})",
+ "request.timeout": "Request timed out",
+ "menu.deep_research": "Deep Research",
+ "menu.create_image": "Create Image",
+ "menu.create_video": "Create Video",
+ "menu.canvas": "Canvas",
+ "sender.quick_actions.add_files": "Add images and files",
+ "sender.quick_actions.upload_tooltip_line1": "Supports PDF/Word/Excel/PPT, images (JPG/PNG), audio/video, text/code files, and compressed files",
+ "sender.quick_actions.upload_tooltip_line2": "Each file ≤100MB, up to 10 files per upload",
+ "sender.quick_actions.upload_ppt_template": "Upload template and copy",
+ "sender.quick_actions.upload_ppt_template_tooltip_template": "Template support: pptx",
+ "sender.quick_actions.upload_ppt_template_tooltip_copy": "Copy support: doc/pdf/txt",
+ "sender.quick_actions.select_knowledge_base": "Select knowledge base",
+ "sender.disabled_tooltip": "Please complete required information",
+ "attachment.view.all": "View all files in this task",
+ "files.all": "All",
+ "files.documents": "Documents",
+ "files.images": "Images",
+ "files.none": "No files",
+ "files.selectAll": "Select All",
+ "files.batch.download": "Batch Download",
+ "files.modal.title": "All files in this task",
+ "files.download.packing": "Packing {{count}} files...",
+ "files.download.success": "Successfully packed and downloaded {{count}} files",
+ "files.download.failed": "Failed to download file: {{filename}}",
+ "files.batch.failed": "Batch download failed, please try again",
+ "button.close": "Close",
+ "knowledge.filter.all": "All",
+ "knowledge.filter.public": "Public",
+ "knowledge.filter.personal": "Personal",
+ "knowledge.placeholder.search": "Search",
+ "knowledge.no_data": "No data",
+ "knowledge.unknown": "Unknown",
+ "knowledge.no_desc": "No description",
+ "preview.creator": "Creator",
+ "preview.view_use_cases": "View Case Studies",
+ "sender.disabled.message": "Task has expired, unable to continue conversation",
+ "sender.disabled.new_chat": "New Chat",
+ "deepresearch.mode.google_api": "Google API Mode",
+ "deepresearch.mode.self_configured": "Self-Configured Mode",
+ "file.preview": "Preview",
+ "file.zoom.in": "Zoom",
+ "file.zoom.out": "Exit zoom",
+ "workspace.collapse": "Collapse",
+ "chatbot.view_trace": "View Trace",
+ "reasoning.thinking": "Thinking",
+ "csv.error.empty": "CSV content is empty",
+ "csv.error.missing_header": "CSV file is missing header",
+ "csv.error.empty_header": "CSV header is empty",
+ "csv.error.zero_columns": "CSV header has zero columns",
+ "csv.error.column_mismatch": "Row {{row}} column count mismatch. Expected {{expected}} columns, got {{actual}} columns",
+ "csv.error.no_data": "CSV file has no data rows",
+ "csv.error.validation_failed": "CSV validation error: {{message}}",
+ "csv.error.unknown": "Unknown error",
+ "csv.format_error": "CSV Format Error",
+ "csv.no_data": "No data",
+ "models.official": "Official Models",
+ "models.my": "My Models",
+ "file.load.failed": "Load Failed",
+ "file.preview.unsupported": "This file cannot be previewed online. Please download to view.",
+ "markdown.parsing": "Parsing Markdown...",
+ "markdown.parse.failed": "Parse Failed",
+ "sender.skills.notFound": "No matching commands"
}
diff --git a/web/src/config/i18n.js b/web/src/config/i18n.js
index a7ef9b4..e10e20e 100644
--- a/web/src/config/i18n.js
+++ b/web/src/config/i18n.js
@@ -3,6 +3,8 @@ import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import en from './en.json';
import zh from './zh.json';
+import kk from './kk.json';
+import ru from './ru.json';
i18n
.use(LanguageDetector)
@@ -27,6 +29,16 @@ i18n
...zh,
},
},
+ kk: {
+ translation: {
+ ...kk,
+ },
+ },
+ ru: {
+ translation: {
+ ...ru,
+ },
+ },
},
});
diff --git a/web/src/config/kk.json b/web/src/config/kk.json
new file mode 100644
index 0000000..194ad42
--- /dev/null
+++ b/web/src/config/kk.json
@@ -0,0 +1,229 @@
+{
+ "welcome": "Қош келдіңіз",
+ "go.back": "Артқа",
+ "feature.future": "Жақында шығады, күтіңіз",
+ "url.404": "URL табылмады",
+ "code.copy": "Көшіру",
+ "code.copy.success": "Жүйе алмасу буферіне көшірілді!",
+ "chatbot.task.success": "Ағымдағы тапсырма орындалды",
+ "chatbot.task.fail": "Ағымдағы тапсырма сәтсіз аяқталды",
+ "chatbot.task.hilt.operate": "Әрекет жасаңыз",
+ "chatbot.task.hilt.operate.finish": "Әрекет аяқталды",
+ "chatbot.task.hilt.continue": "Тапсырманы жалғастыру",
+ "workspace": "Жұмыс аймағы",
+ "tool": "Құрал",
+ "workspace.controller.real-time": "Нақты уақыт",
+ "workspace.controller.jump-to-real-time": "Нақты уақытқа өту",
+ "workspace.task.progress": "Тапсырма барысы",
+ "workspace.task.current_progress": "Ағымдағы барыс",
+ "sender.placeholder": "Маған тапсырма беріңіз",
+ "sender.placeholder.progress": "Тапсырма орындалуда",
+ "sender.knowledge-base": "Білім базасы",
+ "sender.knowledge-base.table.name": "Атауы",
+ "sender.knowledge-base.table.creation-time": "Құрылған уақыты",
+ "sender.btn.mcp": "MCP кеңейтілімі",
+ "sender.mcp.add": "MCP кеңейтілімін қосу",
+ "sender.mcp.selected.knowledge-base": "Таңдалған білім базасы",
+ "sender.mcp.selected.mcp-service": "Таңдалған MCP қызметі",
+ "sender.mcp.selected.sandbox": "Таңдалған қауіпсіз құмсалғыш",
+ "sender.mcp.selected.plugins": "Таңдалған плагиндер",
+ "sender.mcp.selected.workflow": "Таңдалған жұмыс процесі",
+ "sender.mcp.selected.skills": "Таңдалған дағдылар",
+ "sender.replay.restart": "Қайта бастау",
+ "sender.replay.end": "Соңына өту",
+ "button.ok": "Растау",
+ "button.confirm": "Растау",
+ "button.cancel": "Болдырмау",
+ "button.save": "Сақтау",
+ "share.password": "Құпия сөз",
+ "share.authentication": "Ойнату құпия сөзі",
+ "code.preview": "Алдын ала қарау",
+ "code.raw": "Код",
+ "file.upload.format.error": "Файл форматына қолдау көрсетілмейді, келесі форматтарды жүктеңіз:",
+ "file.upload.size.error": "Файл өлшемі аспауы керек",
+ "file.upload.count.error": "Файл саны аспауы керек",
+ "file.upload.attachments": "Қосымшалар",
+ "chatbot.task.thinking": "Ойлануда",
+ "chatbot.task.reconnect.failed": "Қосылым сәтсіз аяқталды, {{count}} рет қайталанды",
+ "chatbot.task.network.offline": "Желі үзілді, желі қосылымын тексеріңіз. Желі қалпына келген соң автоматты түрде жалғасады...",
+ "chatbot.task.network.resuming": "Желі қалпына келтірілді, тапсырма жалғастырылуда...",
+ "chatbot.task.server.reconnecting": "Сервер қосылымы үзілді, қайта қосылуда... ({{current}}/{{max}})",
+ "chatbot.task.connection.interrupted": "Қосылым үзілді, қалпына келтіру әрекеті жасалуда...",
+ "code.diff": "Айырмашылық",
+ "code.old": "Түпнұсқа",
+ "code.new": "Өзгертілген",
+ "user.input.brief": "Жауабыңыздан кейін жұмыс жалғасады",
+ "code.interpreter.execution.result": "Орындалу нәтижесі",
+ "code.interpreter.execution.result.placeholder": "// Орындалу нәтижесі осында көрсетіледі",
+ "error.image.generation.failed": "Сурет жасау сәтсіз аяқталды",
+ "error.password.incorrect": "Құпия сөз қате",
+ "mcp.tool.coming": "Жақында шығады",
+ "tool.tab.mcp": "MCP қызметі",
+ "tool.tab.sandbox": "Қауіпсіз құмсалғыш",
+ "tool.tab.ai-app": "AI қолданбасы",
+ "tool.tab.plugins": "Плагиндер",
+ "tool.tab.workflow": "Жұмыс процесі",
+ "tool.tab.skills": "Дағдылар",
+ "task.finish.reason.completed": "Тапсырма орындалды",
+ "task.finish.reason.failed": "Тапсырма сәтсіз аяқталды",
+ "task.finish.reason.cancelled": "Пайдаланушы бас тартты",
+ "task.finish.reason.abnormal": "Тапсырма қатесі",
+ "task.phone.continue.title": "Бұлттық телефон",
+ "task.phone.continue.text1": "Келесіден",
+ "task.phone.continue.text2": "әрекетті жалғастырыңыз",
+ "task.phone.continue.text3": "Келесіде",
+ "task.phone.continue.text4": "әрекет орындалды, тапсырманы жалғастыру",
+ "task.user_input.continue.button": "Жалғастыру",
+ "task.user_input.take_over_browser.button": "Браузерді ашу үшін басыңыз",
+ "task.replay.replaying": "Тапсырманы қарап шығу...",
+ "task.replay.finished": "Тапсырманы қарап шығу аяқталды!",
+ "sex": "Жынысы",
+ "age": "Жасы",
+ "rateInfo": "Тариф туралы ақпарат",
+ "mcp.config.modal.title": "Кеңейтім параметрлері",
+ "form.placeholder.select": "{{field}} таңдаңыз",
+ "form.placeholder.input": "{{field}} енгізіңіз",
+ "form.placeholder.input.comma": "{{field}} енгізіңіз, бірнеше мәнді үтірмен бөліңіз",
+ "form.placeholder.json": "{{field}} үшін JSON форматын енгізіңіз",
+ "form.switch.yes": "Иә",
+ "form.switch.no": "Жоқ",
+ "form.validation.required": "{{field}} енгізіңіз",
+ "form.validation.minLength": "Кемінде {{min}} таңба",
+ "form.validation.maxLength": "Ең көп дегенде {{max}} таңба",
+ "form.validation.validNumber": "Жарамды санды енгізіңіз",
+ "form.validation.minValue": "Минималды мән: {{min}}",
+ "form.validation.maxValue": "Максималды мән: {{max}}",
+ "form.validation.validEmail": "Жарамды электрондық пошта мекенжайын енгізіңіз",
+ "form.validation.validUrl": "Жарамды URL енгізіңіз",
+ "form.validation.validPhone": "Жарамды телефон нөмірін енгізіңіз",
+ "form.validation.validOptions": "Жарамды опцияларды таңдаңыз",
+ "form.validation.minSelections": "Кем дегенде {{min}} опцияны таңдау керек",
+ "form.validation.maxSelections": "Ең көп дегенде {{max}} опцияны таңдауға болады",
+ "form.submit.loading": "Жіберілуде...",
+ "form.submit.button": "Жіберу",
+ "form.error.checkInput": "Пішін енгізуін тексеріңіз",
+ "user_input.form.submitted.title": "{{title}} жіберілді",
+ "user_input.form.submitted.default_title": "Пішін ақпараты жіберілді",
+ "user_input.form.submitted.field_header": "Өріс",
+ "user_input.form.submitted.value_header": "Мән",
+ "user_input.form.submitted.array_separator": ", ",
+ "user_input.form.submitted.empty_value": "-",
+ "user_input.text.placeholder": "Жауабыңызды енгізіңіз",
+ "user_input.custom.placeholder": "Басқа, енгізіңіз...",
+ "hitl.decision.approve": "мақұлдау",
+ "hitl.decision.reject": "қабылдамау",
+ "user_input.approval.custom.placeholder": "Қабылдамау себебін енгізіңіз",
+ "user_input.none_selected": "Ештеңе таңдалмады",
+ "inputoutput.input.label": "Енгізу",
+ "inputoutput.output.label": "Шығару",
+ "error.image.parser.failed": "Суретті талдау сәтсіз аяқталды",
+ "agent.default.greeting": "Бүгін сізге қалай көмектесе аламын?",
+ "attachment.download": "Жүктеп алу",
+ "sender.stop": "Тоқтату",
+ "reference.default_title": "Сілтеме мазмұны",
+ "reference.draft_label": "Сілтеме жасалды",
+ "reference.remove": "Сілтемені жою",
+ "error.generic": "Қате орын алды",
+ "error.unknown": "Белгісіз қате",
+ "error.image.url_missing": "Сурет мекенжайы жоқ",
+ "files.fetch.failed": "Файлдарды алу сәтсіз аяқталды",
+ "link.copied": "Сілтеме көшірілді",
+ "link.copy_failed": "Көшіру сәтсіз аяқталды",
+ "website_delivery.failed": "Сайтты жеткізу сәтсіз аяқталды",
+ "website_delivery.default_name": "AI сайты",
+ "website_delivery.no_files": "Файлдар жоқ",
+ "website_delivery.no_file_selected": "Файл таңдалмады",
+ "website_delivery.no_preview_url": "Алдын ала қарау URL жоқ",
+ "service_deploy.failed": "Қызметті орналастыру сәтсіз аяқталды",
+ "service_deploy.default_name": "Веб қолданба",
+ "action.refresh": "Жаңарту",
+ "action.share": "Бөлісу",
+ "workspace.open": "Жұмыс аймағын ашу",
+ "workspace.collapse_task_progress": "Тапсырма барысын жиыру",
+ "workspace.expand_task_progress": "Тапсырма барысын кеңейту",
+ "share.fetch_failed": "Бөлісу мәліметтерін алу сәтсіз аяқталды",
+ "welcome.title": "Welcome to LangCrew",
+ "welcome.description": "Provide the agent with a mission.",
+ "home.welcome.title": "Wanzhi супер қызметкерлеріне қош келдіңіз",
+ "home.welcome.desc": "Салаңызға арналған интеллектуалды шешімдер үшін кәсіби супер қызметкерді таңдаңыз",
+ "home.apply": "Қосылуға өтініш беру",
+ "home.cases.title": "Wanzhi клиенттерінің үздік кейстері",
+ "home.core_capability": "Негізгі мүмкіндіктер",
+ "home.coming_soon": "Көбірек супер қызметкерлер дайындалуда, күтіңіз!",
+ "fallback.404.title": "404",
+ "fallback.404.subtitle": "Кешіріңіз, сіз кірген бет жоқ.",
+ "cloudphone.sdk.load_failed": "SDK жүктеу сәтсіз аяқталды",
+ "cloudphone.sdk.not_found": "SDK табылмады",
+ "request.failed": "Сұрау сәтсіз аяқталды",
+ "request.network_error": "Желі қатесі",
+ "request.bad_request": "Сұрау параметрлері қате",
+ "request.unauthorized": "Рұқсат жоқ, қайта кіріңіз",
+ "request.forbidden": "Қатынауға тыйым салынды",
+ "request.not_found": "Сұралған ресурс жоқ",
+ "request.server_error": "Сервердің ішкі қатесі",
+ "request.bad_gateway": "Шлюз қатесі",
+ "request.service_unavailable": "Қызмет қолжетімсіз",
+ "request.failed_with_status": "Сұрау сәтсіз аяқталды ({{status}})",
+ "request.timeout": "Сұрау уақыты аяқталды",
+ "menu.deep_research": "Терең зерттеу",
+ "menu.create_image": "Сурет жасау",
+ "menu.create_video": "Бейне жасау",
+ "menu.canvas": "Кенеп",
+ "sender.quick_actions.add_files": "Суреттер мен файлдарды қосу",
+ "sender.quick_actions.upload_tooltip_line1": "PDF/Word/Excel/PPT, суреттер (JPG/PNG), аудио/бейне, мәтін/код файлдарын, сығылып қойылған файлдарды қолдайды",
+ "sender.quick_actions.upload_tooltip_line2": "Бір файл ≤100МБ, бір ретте ең көп 10 файл",
+ "sender.quick_actions.upload_ppt_template": "Үлгі мен мәтінді жүктеу",
+ "sender.quick_actions.upload_ppt_template_tooltip_template": "Үлгі қолдауы: pptx",
+ "sender.quick_actions.upload_ppt_template_tooltip_copy": "Мәтін қолдауы: doc/pdf/txt",
+ "sender.quick_actions.select_knowledge_base": "Білім базасын таңдау",
+ "sender.disabled_tooltip": "Міндетті ақпаратты толтырыңыз",
+ "attachment.view.all": "Осы тапсырмадағы барлық файлдарды қарау",
+ "files.all": "Барлығы",
+ "files.documents": "Құжаттар",
+ "files.images": "Суреттер",
+ "files.none": "Файлдар жоқ",
+ "files.selectAll": "Барлығын таңдау",
+ "files.batch.download": "Жаппай жүктеп алу",
+ "files.modal.title": "Осы тапсырмадағы барлық файлдарды қарау",
+ "files.download.packing": "{{count}} файл жиналуда...",
+ "files.download.success": "{{count}} файл сәтті жиналып, жүктелді",
+ "files.download.failed": "Файлды жүктеу сәтсіз аяқталды: {{filename}}",
+ "files.batch.failed": "Жаппай жүктеп алу сәтсіз аяқталды, қайталап көріңіз",
+ "button.close": "Жабу",
+ "knowledge.filter.all": "Барлығы",
+ "knowledge.filter.public": "Ашық",
+ "knowledge.filter.personal": "Жеке",
+ "knowledge.placeholder.search": "Іздеу",
+ "knowledge.no_data": "Деректер жоқ",
+ "knowledge.unknown": "Белгісіз",
+ "knowledge.no_desc": "Сипаттама жоқ",
+ "preview.creator": "Құрушы",
+ "preview.view_use_cases": "Пайдалану жағдайларын қарау",
+ "sender.disabled.message": "Тапсырманың мерзімі өтті, диалогты жалғастыру мүмкін емес",
+ "sender.disabled.new_chat": "Жаңа диалог",
+ "deepresearch.mode.google_api": "Google API режимі",
+ "deepresearch.mode.self_configured": "Пайдаланушы режимі",
+ "file.preview": "Алдын ала қарау",
+ "file.zoom.in": "Масштабтау",
+ "file.zoom.out": "Масштабтаудан шығу",
+ "workspace.collapse": "Жиыру",
+ "chatbot.view_trace": "Трекіні қарау",
+ "reasoning.thinking": "Ойлануда",
+ "csv.error.empty": "CSV мазмұны бос",
+ "csv.error.missing_header": "CSV файлда тақырып жоқ",
+ "csv.error.empty_header": "CSV тақырыбы бос",
+ "csv.error.zero_columns": "CSV тақырыбында 0 баған",
+ "csv.error.column_mismatch": "{{row}} жолдың бағандарының саны сәйкес келмейді. Күтілген {{expected}} баған, нақты {{actual}} баған",
+ "csv.error.no_data": "CSV файлда деректер жолдары жоқ",
+ "csv.error.validation_failed": "CSV тексеру қатесі: {{message}}",
+ "csv.error.unknown": "Белгісіз қате",
+ "csv.format_error": "CSV форматы қатесі",
+ "csv.no_data": "Деректер жоқ",
+ "models.official": "Ресми модельдер",
+ "models.my": "Менің модельдерім",
+ "file.load.failed": "Жүктеу сәтсіз аяқталды",
+ "file.preview.unsupported": "Бұл файлды онлайн қарау мүмкін емес. Көру үшін жүктеп алыңыз.",
+ "markdown.parsing": "Markdown талдануда...",
+ "markdown.parse.failed": "Талдау сәтсіз аяқталды",
+ "sender.skills.notFound": "Сәйкес пәрмен жоқ"
+}
diff --git a/web/src/config/resources.js b/web/src/config/resources.js
index ebf8a90..90f7497 100644
--- a/web/src/config/resources.js
+++ b/web/src/config/resources.js
@@ -1,9 +1,11 @@
import en from './en.json';
import zh from './zh.json';
import ru from './ru.json';
+import kk from './kk.json';
export const resources = {
en,
zh,
ru,
+ kk,
};
diff --git a/web/src/config/ru.json b/web/src/config/ru.json
index 74c3c07..fa6ee7e 100644
--- a/web/src/config/ru.json
+++ b/web/src/config/ru.json
@@ -15,6 +15,7 @@
"workspace.controller.real-time": "Реальное время",
"workspace.controller.jump-to-real-time": "Перейти к реальному времени",
"workspace.task.progress": "Ход выполнения задачи",
+ "workspace.task.current_progress": "Прогресс ",
"sender.placeholder": "Пожалуйста, дайте мне задачу",
"sender.placeholder.progress": "Выполнение задачи",
"sender.knowledge-base": "База знаний",
@@ -27,9 +28,11 @@
"sender.mcp.selected.sandbox": "Выбранная песочница",
"sender.mcp.selected.plugins": "Выбранные плагины",
"sender.mcp.selected.workflow": "Выбранный рабочий процесс",
+ "sender.mcp.selected.skills": "Выбранные навыки",
"sender.replay.restart": "Перезапустить",
"sender.replay.end": "Перейти к концу",
- "button.ok": "ОК",
+ "button.ok": "Подтвердить",
+ "button.save": "Сохранить",
"button.confirm": "Подтвердить",
"button.cancel": "Отмена",
"share.password": "Пароль",
@@ -41,6 +44,11 @@
"file.upload.count.error": "Количество файлов не может превышать",
"file.upload.attachments": "Вложения",
"chatbot.task.thinking": "Думает...",
+ "chatbot.task.reconnect.failed": "Не удалось подключиться после {{count}} попыток",
+ "chatbot.task.network.offline": "Сеть отключена. Автоматически продолжится при восстановлении сети...",
+ "chatbot.task.network.resuming": "Сеть восстановлена, продолжение задачи...",
+ "chatbot.task.server.reconnecting": "Соединение с сервером потеряно, повторное подключение... ({{current}}/{{max}})",
+ "chatbot.task.connection.interrupted": "Соединение прервано, попытка восстановления...",
"code.diff": "Различия",
"code.old": "Исходный",
"code.new": "Изменённый",
@@ -55,6 +63,7 @@
"tool.tab.ai-app": "AI-приложение",
"tool.tab.plugins": "Плагины",
"tool.tab.workflow": "Рабочий процесс",
+ "tool.tab.skills": "Навыки",
"task.finish.reason.completed": "Текущая задача завершена",
"task.finish.reason.failed": "Задача не выполнена",
"task.finish.reason.cancelled": "Пользователь отменил",
@@ -92,5 +101,129 @@
"form.validation.maxSelections": "Можно выбрать максимум {{max}} вариантов",
"form.submit.loading": "Отправка...",
"form.submit.button": "Отправить",
- "form.error.checkInput": "Проверьте введённые данные"
+ "form.error.checkInput": "Проверьте введённые данные",
+ "user_input.form.submitted.title": "Отправлено: {{title}}",
+ "user_input.form.submitted.default_title": "Данные формы отправлены",
+ "user_input.form.submitted.field_header": "Поле",
+ "user_input.form.submitted.value_header": "Значение",
+ "user_input.form.submitted.array_separator": ", ",
+ "user_input.form.submitted.empty_value": "-",
+ "user_input.text.placeholder": "Введите ответ",
+ "user_input.custom.placeholder": "Другое, введите...",
+ "hitl.decision.approve": "одобрить",
+ "hitl.decision.reject": "отклонить",
+ "user_input.approval.custom.placeholder": "Введите причину отклонения",
+ "user_input.none_selected": "Ничего не выбрано",
+ "inputoutput.input.label": "Ввод",
+ "inputoutput.output.label": "Вывод",
+ "error.image.parser.failed": "Ошибка разбора изображения",
+ "agent.default.greeting": "Чем я могу помочь сегодня?",
+ "attachment.download": "Скачать",
+ "sender.stop": "Остановить",
+ "reference.default_title": "Содержимое ссылки",
+ "reference.draft_label": "Добавлено в ссылку",
+ "reference.remove": "Удалить ссылку",
+ "error.generic": "Произошла ошибка",
+ "error.unknown": "Неизвестная ошибка",
+ "error.image.url_missing": "Адрес изображения отсутствует",
+ "files.fetch.failed": "Не удалось получить файлы",
+ "link.copied": "Ссылка скопирована",
+ "link.copy_failed": "Не удалось скопировать",
+ "website_delivery.failed": "Не удалось доставить сайт",
+ "website_delivery.default_name": "AI-сайт",
+ "website_delivery.no_files": "Нет файлов",
+ "website_delivery.no_file_selected": "Файл не выбран",
+ "website_delivery.no_preview_url": "Нет URL предпросмотра",
+ "service_deploy.failed": "Не удалось развернуть сервис",
+ "service_deploy.default_name": "Веб-приложение",
+ "action.refresh": "Обновить",
+ "action.share": "Поделиться",
+ "workspace.open": "Открыть рабочую область",
+ "workspace.collapse_task_progress": "Свернуть ход задачи",
+ "workspace.expand_task_progress": "Развернуть ход задачи",
+ "share.fetch_failed": "Не удалось получить данные общего доступа",
+ "welcome.title": "Welcome to LangCrew",
+ "welcome.description": "Provide the agent with a mission.",
+ "home.welcome.title": "Добро пожаловать в Wanzhi Super Employees",
+ "home.welcome.desc": "Выберите профессионального супер-сотрудника для интеллектуальных решений под вашу отрасль",
+ "home.apply": "Подать заявку",
+ "home.cases.title": "Лучшие кейсы клиентов Wanzhi",
+ "home.core_capability": "Ключевые возможности",
+ "home.coming_soon": "Больше супер-сотрудников уже готовится. Следите за обновлениями!",
+ "fallback.404.title": "404",
+ "fallback.404.subtitle": "Извините, запрошенная страница не существует.",
+ "cloudphone.sdk.load_failed": "Не удалось загрузить SDK",
+ "cloudphone.sdk.not_found": "SDK не найден",
+ "request.failed": "Запрос не выполнен",
+ "request.network_error": "Ошибка сети",
+ "request.bad_request": "Некорректные параметры запроса",
+ "request.unauthorized": "Не авторизовано, войдите снова",
+ "request.forbidden": "Доступ запрещён",
+ "request.not_found": "Запрошенный ресурс не существует",
+ "request.server_error": "Внутренняя ошибка сервера",
+ "request.bad_gateway": "Ошибка шлюза",
+ "request.service_unavailable": "Сервис недоступен",
+ "request.failed_with_status": "Запрос не выполнен ({{status}})",
+ "request.timeout": "Время запроса истекло",
+ "menu.deep_research": "Глубокое исследование",
+ "menu.create_image": "Создать изображение",
+ "menu.create_video": "Создать видео",
+ "menu.canvas": "Холст",
+ "attachment.view.all": "Просмотреть все файлы в этой задаче",
+ "files.all": "Все",
+ "files.documents": "Документы",
+ "files.images": "Изображения",
+ "files.none": "Нет файлов",
+ "files.selectAll": "Выбрать все",
+ "files.batch.download": "Массовая загрузка",
+ "files.modal.title": "Все файлы в этой задаче",
+ "files.download.packing": "Упаковка {{count}} файлов...",
+ "files.download.success": "Успешно упаковано и загружено {{count}} файлов",
+ "files.download.failed": "Не удалось загрузить файл: {{filename}}",
+ "files.batch.failed": "Не удалось выполнить массовую загрузку, попробуйте еще раз",
+ "button.close": "Закрыть",
+ "knowledge.filter.all": "Все",
+ "knowledge.filter.public": "Общедоступный",
+ "knowledge.filter.personal": "Личные",
+ "knowledge.placeholder.search": "Поиск",
+ "knowledge.no_data": "Нет данных",
+ "knowledge.unknown": "Неизвестно",
+ "knowledge.no_desc": "Нет описания",
+ "sender.quick_actions.add_files": "Добавить изображения и файлы",
+ "sender.quick_actions.upload_tooltip_line1": "Поддерживает PDF/Word/Excel/PPT, изображения (JPG/PNG), аудио/видео, текстовые и программные файлы, сжатые файлы",
+ "sender.quick_actions.upload_tooltip_line2": "Каждый файл ≤100MB, до 10 файлов за раз",
+ "sender.quick_actions.upload_ppt_template": "Загрузить шаблон и текст",
+ "sender.quick_actions.upload_ppt_template_tooltip_template": "Поддержка шаблонов: pptx",
+ "sender.quick_actions.upload_ppt_template_tooltip_copy": "Поддержка текстов: doc/pdf/txt",
+ "sender.quick_actions.select_knowledge_base": "Выбрать базу знаний",
+ "sender.disabled_tooltip": "Пожалуйста, заполните обязательную информацию",
+ "sender.disabled.message": "Задача истекла, продолжение разговора невозможно",
+ "sender.disabled.new_chat": "Новый разговор",
+ "preview.creator": "Создатель",
+ "preview.view_use_cases": "Просмотреть варианты использования",
+ "deepresearch.mode.google_api": "Режим Google API",
+ "deepresearch.mode.self_configured": "Пользовательский режим",
+ "file.preview": "Предпросмотр",
+ "file.zoom.in": "Масштабировать",
+ "file.zoom.out": "Выйти из режима масштабирования",
+ "workspace.collapse": "Свернуть",
+ "chatbot.view_trace": "Просмотреть трек",
+ "reasoning.thinking": "Размышление",
+ "csv.error.empty": "CSV файл пустой",
+ "csv.error.missing_header": "CSV файл не содержит заголовка",
+ "csv.error.empty_header": "Заголовок CSV пустой",
+ "csv.error.zero_columns": "Заголовок CSV содержит 0 столбцов",
+ "csv.error.column_mismatch": "Строка {{row}} не соответствует количеству столбцов. Ожидается {{expected}} столбцов, получено {{actual}} столбцов",
+ "csv.error.no_data": "CSV файл не содержит строк данных",
+ "csv.error.validation_failed": "Ошибка проверки CSV: {{message}}",
+ "csv.error.unknown": "Неизвестная ошибка",
+ "csv.format_error": "Ошибка формата CSV",
+ "csv.no_data": "Нет данных",
+ "models.official": "Официальный модели",
+ "models.my": "Мой модели",
+ "file.load.failed": "Не удалось загрузить",
+ "file.preview.unsupported": "Этот файл нельзя просмотреть онлайн. Скачайте для просмотра.",
+ "markdown.parsing": "Разбор Markdown...",
+ "markdown.parse.failed": "Разбор не удался",
+ "sender.skills.notFound": "Нет совпадений"
}
diff --git a/web/src/config/zh.json b/web/src/config/zh.json
index 060b5b1..db42c43 100644
--- a/web/src/config/zh.json
+++ b/web/src/config/zh.json
@@ -15,6 +15,7 @@
"workspace.controller.real-time": "实时",
"workspace.controller.jump-to-real-time": "跳到实时",
"workspace.task.progress": "任务进度",
+ "workspace.task.current_progress": "当前进度",
"sender.placeholder": "请给我一个任务",
"sender.placeholder.progress": "任务执行中",
"sender.knowledge-base": "知识库",
@@ -27,11 +28,13 @@
"sender.mcp.selected.sandbox": "已选安全沙箱",
"sender.mcp.selected.plugins": "已选插件",
"sender.mcp.selected.workflow": "已选工作流",
+ "sender.mcp.selected.skills": "已选技能",
"sender.replay.restart": "重新开始",
- "sender.replay.end": "跳到结束",
+ "sender.replay.end": "跳到最后",
"button.ok": "确定",
"button.confirm": "确认",
"button.cancel": "取消",
+ "button.save": "保存",
"share.password": "密码",
"share.authentication": "回放密码",
"code.preview": "预览",
@@ -41,6 +44,11 @@
"file.upload.count.error": "文件数量不能超过",
"file.upload.attachments": "附件",
"chatbot.task.thinking": "思考中",
+ "chatbot.task.reconnect.failed": "连接失败,已重试 {{count}} 次",
+ "chatbot.task.network.offline": "网络已断开,请检查网络连接。网络恢复后将自动继续...",
+ "chatbot.task.network.resuming": "网络已恢复,正在继续任务...",
+ "chatbot.task.server.reconnecting": "服务器连接中断,正在重连... ({{current}}/{{max}})",
+ "chatbot.task.connection.interrupted": "连接中断,正在尝试恢复...",
"code.diff": "差异",
"code.old": "原始",
"code.new": "修改后",
@@ -55,7 +63,8 @@
"tool.tab.ai-app": "AI应用",
"tool.tab.plugins": "插件",
"tool.tab.workflow": "工作流",
- "task.finish.reason.completed": "已完成当前任务",
+ "tool.tab.skills": "技能",
+ "task.finish.reason.completed": "任务已完成",
"task.finish.reason.failed": "任务失败",
"task.finish.reason.cancelled": "用户已取消",
"task.finish.reason.abnormal": "任务异常",
@@ -93,6 +102,128 @@
"form.submit.loading": "提交中...",
"form.submit.button": "提交",
"form.error.checkInput": "请检查表单输入",
+ "user_input.form.submitted.title": "已提交{{title}}",
+ "user_input.form.submitted.default_title": "已提交表单信息",
+ "user_input.form.submitted.field_header": "字段",
+ "user_input.form.submitted.value_header": "内容",
+ "user_input.form.submitted.array_separator": "、",
+ "user_input.form.submitted.empty_value": "-",
+ "user_input.text.placeholder": "请输入回复",
+ "user_input.custom.placeholder": "其他情况,请输入...",
+ "hitl.decision.approve": "同意",
+ "hitl.decision.reject": "拒绝",
+ "user_input.approval.custom.placeholder": "请输入拒绝原因",
+ "user_input.none_selected": "未选择任何选项",
"inputoutput.input.label": "输入",
- "inputoutput.output.label": "输出"
+ "inputoutput.output.label": "输出",
+ "error.image.parser.failed": "图片解析失败",
+ "agent.default.greeting": "今天能为你做些什么?",
+ "attachment.download": "下载",
+ "sender.stop": "停止",
+ "reference.default_title": "引用内容",
+ "reference.draft_label": "已引用",
+ "reference.remove": "移除引用",
+ "error.generic": "发生了一个错误",
+ "error.unknown": "未知错误",
+ "error.image.url_missing": "图片地址不存在",
+ "files.fetch.failed": "获取文件失败",
+ "link.copied": "链接已复制",
+ "link.copy_failed": "复制失败",
+ "website_delivery.failed": "网站交付失败",
+ "website_delivery.default_name": "AI网站",
+ "website_delivery.no_files": "暂无文件",
+ "website_delivery.no_file_selected": "未选择文件",
+ "website_delivery.no_preview_url": "暂无预览地址",
+ "service_deploy.failed": "服务部署失败",
+ "service_deploy.default_name": "Web 应用",
+ "action.refresh": "刷新",
+ "action.share": "分享",
+ "workspace.open": "打开工作区",
+ "workspace.collapse_task_progress": "收起任务进度",
+ "workspace.expand_task_progress": "展开任务进度",
+ "share.fetch_failed": "获取分享详情失败",
+ "welcome.title": "Welcome to LangCrew",
+ "welcome.description": "Provide the agent with a mission.",
+ "home.welcome.title": "欢迎体验万智超级员工",
+ "home.welcome.desc": "选择专业的超级员工,为您的行业提供定制化的智能解决方案",
+ "home.apply": "申请接入",
+ "home.cases.title": "来自万智客户的精彩案例",
+ "home.core_capability": "核心能力",
+ "home.coming_soon": "更多超级员工正在生产中,敬请期待!",
+ "fallback.404.title": "404",
+ "fallback.404.subtitle": "抱歉,您访问的页面不存在。",
+ "cloudphone.sdk.load_failed": "SDK 加载失败",
+ "cloudphone.sdk.not_found": "SDK 未找到",
+ "request.failed": "请求失败",
+ "request.network_error": "网络错误",
+ "request.bad_request": "请求参数错误",
+ "request.unauthorized": "未授权,请重新登录",
+ "request.forbidden": "拒绝访问",
+ "request.not_found": "请求的资源不存在",
+ "request.server_error": "服务器内部错误",
+ "request.bad_gateway": "网关错误",
+ "request.service_unavailable": "服务不可用",
+ "request.failed_with_status": "请求失败 ({{status}})",
+ "request.timeout": "请求超时",
+ "menu.deep_research": "深度研究",
+ "menu.create_image": "创建图片",
+ "menu.create_video": "创建视频",
+ "menu.canvas": "画布",
+ "sender.quick_actions.add_files": "添加图片和文件",
+ "sender.quick_actions.upload_tooltip_line1": "支持 PDF/Word/Excel/PPT、图片(JPG/PNG)、音视频、文本/代码文件、压缩文件",
+ "sender.quick_actions.upload_tooltip_line2": "单个文件≤100MB,单次最多10个",
+ "sender.quick_actions.upload_ppt_template": "上传模板和文案",
+ "sender.quick_actions.upload_ppt_template_tooltip_template": "模版支持: pptx",
+ "sender.quick_actions.upload_ppt_template_tooltip_copy": "文案支持: doc/pdf/txt",
+ "sender.quick_actions.select_knowledge_base": "选择知识库",
+ "sender.disabled_tooltip": "请完成必填信息",
+ "attachment.view.all": "查看此任务中的所有文件",
+ "files.all": "全部",
+ "files.documents": "文档",
+ "files.images": "图片",
+ "files.none": "暂无文件",
+ "files.selectAll": "全选",
+ "files.batch.download": "批量下载",
+ "files.modal.title": "此任务中的所有文件",
+ "files.download.packing": "正在打包 {{count}} 个文件...",
+ "files.download.success": "成功打包下载 {{count}} 个文件",
+ "files.download.failed": "下载文件失败: {{filename}}",
+ "files.batch.failed": "打包下载失败,请重试",
+ "button.close": "关闭",
+ "knowledge.filter.all": "全部",
+ "knowledge.filter.public": "公开",
+ "knowledge.filter.personal": "个人",
+ "knowledge.placeholder.search": "搜索",
+ "knowledge.no_data": "暂无数据",
+ "knowledge.unknown": "未知",
+ "knowledge.no_desc": "暂无描述",
+ "preview.creator": "创建者",
+ "preview.view_use_cases": "查看使用案例",
+ "sender.disabled.message": "任务已失效,无法继续对话",
+ "sender.disabled.new_chat": "新对话",
+ "deepresearch.mode.google_api": "Google API 模式",
+ "deepresearch.mode.self_configured": "自定义模式",
+ "file.preview": "预览",
+ "file.zoom.in": "缩放",
+ "file.zoom.out": "退出缩放",
+ "workspace.collapse": "收起",
+ "chatbot.view_trace": "View Trace",
+ "reasoning.thinking": "思考",
+ "csv.error.empty": "CSV 内容为空",
+ "csv.error.missing_header": "CSV 文件缺少表头",
+ "csv.error.empty_header": "CSV 表头为空",
+ "csv.error.zero_columns": "CSV 表头列数为0",
+ "csv.error.column_mismatch": "第 {{row}} 行列数不匹配。期望 {{expected}} 列,实际 {{actual}} 列",
+ "csv.error.no_data": "CSV 文件没有数据行",
+ "csv.error.validation_failed": "CSV 验证错误: {{message}}",
+ "csv.error.unknown": "未知错误",
+ "csv.format_error": "CSV 格式错误",
+ "csv.no_data": "暂无数据",
+ "models.official": "官方模型",
+ "models.my": "我的模型",
+ "file.load.failed": "加载失败",
+ "file.preview.unsupported": "此文件暂不支持在线预览,请下载后查看",
+ "markdown.parsing": "正在解析 Markdown...",
+ "markdown.parse.failed": "解析失败",
+ "sender.skills.notFound": "无匹配指令"
}
diff --git a/web/src/engines/README.md b/web/src/engines/README.md
new file mode 100644
index 0000000..f438511
--- /dev/null
+++ b/web/src/engines/README.md
@@ -0,0 +1,307 @@
+# JSON-to-JSX Widget Rendering Engine
+
+A core engine that converts JSON configurations into React JSX components.
+
+## Overview
+
+This engine allows generating complete React component trees from simple JSON configurations or JSX strings. Supports both JSON and JSX formats for flexible dynamic UI generation.
+
+## Core Features
+
+- ✅ **Recursive JSON to JSX conversion** - Automatically convert nested JSON to React component trees
+- ✅ **Component registration system** - Support registering custom components
+- ✅ **Type safety** - Complete TypeScript support
+- ✅ **Zero runtime overhead** - Converted to standard React components
+- ✅ **Extensible** - Easily add new component types
+
+## Usage
+
+### Basic Usage
+
+```typescript
+import { renderWidget } from '@/engines/jsonToJsx';
+
+const widgetData = {
+ key: 'weather',
+ type: 'Card',
+ children: [
+ {
+ type: 'Text',
+ value: 'Hello World',
+ weight: 'semibold',
+ size: 'lg',
+ },
+ ],
+};
+
+export function MyComponent() {
+ const element = renderWidget(widgetData);
+ return {element}
;
+}
+```
+
+### Generic Widget Data Rendering
+
+```typescript
+import { jsonToJsx } from '@/engines/jsonToJsx';
+
+// Widget data from any source (message detail, props, etc.)
+const widgetData = message.detail;
+
+// Supports both direct widget data and wrapped format
+const element = jsonToJsx(widgetData);
+```
+
+### Register Custom Components
+
+```typescript
+import { registerComponent } from '@/engines/jsonToJsx';
+import MyCustomComponent from './MyCustomComponent';
+
+// After registering, can use in JSON
+registerComponent('MyComponent', MyCustomComponent);
+
+const data = {
+ type: 'MyComponent',
+ customProp: 'value',
+};
+```
+
+## Supported Component Types
+
+| Component | Description | Example |
+| --------- | ----------------- | --------------------------------------------------- |
+| `Card` | Root container | `{ type: 'Card', padding: 5 }` |
+| `Box` | Generic container | `{ type: 'Box', gap: 2, padding: 3 }` |
+| `Row` | Horizontal layout | `{ type: 'Row', align: 'center' }` |
+| `Col` | Vertical layout | `{ type: 'Col', gap: 1 }` |
+| `Text` | Text | `{ type: 'Text', value: 'Hello', size: 'lg' }` |
+| `Title` | Title | `{ type: 'Title', value: 'Title', weight: 'bold' }` |
+| `Image` | Image | `{ type: 'Image', src: 'url', alt: 'desc' }` |
+
+## JSON Data Structure
+
+### Container Component Properties
+
+```typescript
+interface ContainerProps {
+ type: 'Card' | 'Box' | 'Row' | 'Col';
+
+ // Layout
+ gap?: number; // rem unit spacing
+ padding?: number; // rem unit padding
+ align?: 'start' | 'center' | 'end' | 'stretch';
+ justify?: 'start' | 'center' | 'end' | 'between' | 'around';
+
+ // Size
+ width?: number | string; // px or percentage
+ minWidth?: number; // px
+ maxWidth?: number; // px
+ minHeight?: number; // px
+ maxHeight?: number; // px
+
+ // Style
+ background?: 'surface-tertiary' | 'blue-100';
+ radius?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
+
+ // Content
+ children?: WidgetComponent[];
+}
+```
+
+### Text Component Properties
+
+```typescript
+interface TextProps {
+ type: 'Text' | 'Title';
+ value: string; // Text content
+ weight?: 'normal' | 'medium' | 'semibold' | 'bold';
+ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+ color?: 'primary' | 'secondary' | 'tertiary';
+}
+```
+
+### Image Component Properties
+
+```typescript
+interface ImageProps {
+ type: 'Image';
+ src: string; // Image URL
+ alt: string; // Alternative text
+ fit?: 'contain' | 'cover' | 'fill';
+ size?: number; // px
+}
+```
+
+## Example: Weather Card
+
+```json
+{
+ "type": "Card",
+ "children": [
+ {
+ "type": "Box",
+ "padding": 5,
+ "background": "surface-tertiary",
+ "children": [
+ {
+ "type": "Row",
+ "align": "center",
+ "justify": "between",
+ "children": [
+ {
+ "type": "Col",
+ "gap": 1,
+ "children": [
+ {
+ "type": "Text",
+ "value": "Paris",
+ "weight": "semibold",
+ "size": "lg"
+ }
+ ]
+ },
+ {
+ "type": "Image",
+ "src": "weather-icon.svg",
+ "alt": "Sunny",
+ "size": 28
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Architecture
+
+```
+JSON Input
+ ↓
+jsonToJsx / renderWidget
+ ↓
+renderWidgetComponent (recursive)
+ ↓
+componentMap (query from registry)
+ ↓
+React.createElement
+ ↓
+JSX Element ✓
+```
+
+## Extension Guide
+
+### Adding New Component Type
+
+1. Create component file `src/components/widgets/MyComponent.tsx`
+2. Export React component
+3. Register in the engine:
+ ```typescript
+ import MyComponent from '@/components/widgets/MyComponent';
+ registerComponent('MyComponent', MyComponent);
+ ```
+
+### Extend Type Definitions
+
+Edit `src/types/widget.ts`:
+
+```typescript
+// Add new component type
+export type ComponentType =
+ | 'Card' | 'Box' | ...
+ | 'MyComponent'; // ← New
+
+// Define component properties
+export interface MyComponentProps {
+ type: 'MyComponent';
+ customProp: string;
+}
+
+// Add to union type
+export type WidgetComponent =
+ | ...
+ | MyComponentProps; // ← New
+```
+
+## Color System
+
+Define CSS variables in `src/components/widgets/widgets.css`:
+
+```css
+:root {
+ --color-text-primary: #000000;
+ --color-text-secondary: #666666;
+ --color-text-tertiary: #999999;
+ --color-surface-tertiary: #f5f5f5;
+ --blue-100: #dbeafe;
+}
+```
+
+Customize by modifying these variables.
+
+## Performance Optimization
+
+- Components use `React.createElement` for dynamic creation, support Memo wrapping
+- Automatic key handling during recursion
+- Registry lookup is O(1) operation
+
+## Debugging
+
+Enable development mode to view component tree:
+
+```typescript
+// Engine already has console.warn check for unregistered components
+// Missing components will output warning:
+// "Component type 'UnknownType' not registered"
+```
+
+## FAQ
+
+**Q: How to modify styles?**
+A: Modify CSS variables in `src/components/widgets/widgets.css` or style mappings in components.
+
+**Q: How to add new colors?**
+A: Extend `BackgroundColor` and `TextColor` types in types, add CSS variables, update color mappings.
+
+**Q: Does it support responsive design?**
+A: Currently basic support, can extend with media query support through component props.
+
+## File Structure
+
+```
+src/
+├── engines/
+│ ├── jsonToJsx.ts # Core rendering engine
+│ ├── jsonToJsx.test.ts # Tests
+│ └── README.md # This file
+│
+├── types/
+│ └── widget.ts # Complete type definitions
+│
+└── components/widgets/
+ ├── Card.tsx # Container components
+ ├── Box.tsx
+ ├── Row.tsx
+ ├── Col.tsx
+ ├── Text.tsx # Text components
+ ├── Title.tsx
+ ├── Image.tsx # Image components
+ ├── widgets.css # Styles
+ └── index.ts # Exports
+```
+
+## Contributing Guide
+
+Contributions welcome! Please follow these steps:
+
+1. Create a feature branch
+2. Add TypeScript types
+3. Implement components
+4. Add test cases
+5. Submit Pull Request
+
+---
+
+Last updated: 2025-10-31
diff --git a/web/src/engines/jsonToJsx.ts b/web/src/engines/jsonToJsx.ts
new file mode 100644
index 0000000..ccdfc6a
--- /dev/null
+++ b/web/src/engines/jsonToJsx.ts
@@ -0,0 +1,201 @@
+import React, { createElement, isValidElement } from 'react';
+import * as Babel from '@babel/standalone';
+
+// Layout components
+import Box from '@/components/widgets/Box';
+import Row from '@/components/widgets/Row';
+import Col from '@/components/widgets/Col';
+import Card from '@/components/widgets/Card';
+
+// Text components
+import Text from '@/components/widgets/Text';
+import Title from '@/components/widgets/Title';
+import Caption from '@/components/widgets/Caption';
+import Label from '@/components/widgets/Label';
+
+// Media components
+import Image from '@/components/widgets/Image';
+import Icon from '@/components/widgets/Icon';
+
+// Form components
+import Button from '@/components/widgets/Button';
+import DatePicker from '@/components/widgets/DatePicker';
+import Select from '@/components/widgets/Select';
+import Checkbox from '@/components/widgets/Checkbox';
+import RadioGroup from '@/components/widgets/RadioGroup';
+import Input from '@/components/widgets/Input';
+import Textarea from '@/components/widgets/Textarea';
+import Form from '@/components/widgets/Form';
+
+// Content components
+import Markdown from '@/components/widgets/Markdown';
+
+// Data visualization components
+import Chart from '@/components/widgets/Chart';
+
+// Utility components
+import Spacer from '@/components/widgets/Spacer';
+import Divider from '@/components/widgets/Divider';
+import Badge from '@/components/widgets/Badge';
+import { JsxJson } from '@/types';
+import ListView from '@/components/widgets/ListView';
+
+import '@/components/widgets/widgets.css';
+import ListViewItem from '@/components/widgets/ListViewItem';
+import Transition from '@/components/widgets/Transition';
+
+/**
+ * Component map for registering custom components
+ * Automatically includes all widget components
+ *
+ * IMPORTANT: This map must include all ComponentType values from types/widget.ts
+ * If you add a new component:
+ * 1. Export it from widgets/index.ts
+ * 2. Add it to ComponentType union in types/widget.ts
+ * 3. Import and add it to this componentMap
+ */
+const componentMap: Record> = {
+ // Layout components
+ Card,
+ Box,
+ Row,
+ Col,
+
+ // Text components
+ Text,
+ Title,
+ Caption,
+ Label,
+
+ // Media components
+ Image,
+ Icon,
+
+ // Form components
+ Button,
+ DatePicker,
+ Select,
+ Checkbox,
+ RadioGroup,
+ Input,
+ Textarea,
+ Form,
+
+ // Content components
+ Markdown,
+
+ // Data visualization components
+ Chart,
+
+ // Utility components
+ Spacer,
+ Divider,
+ Badge,
+ ListView,
+ ListViewItem,
+
+ Transition,
+};
+
+/**
+ * Convert JSON widget data to React element
+ * Supports both direct widget data and wrapped format
+ */
+export function jsonToJsx(
+ data: JsxJson,
+ customComponents: Record> = {},
+ root = true,
+): React.ReactElement | null {
+ if (!data || !data.type) {
+ return null;
+ }
+
+ const component = data;
+
+ // Merge custom components with default component map
+ const components = {
+ ...componentMap,
+ ...customComponents,
+ };
+
+ const ComponentType = components[component.type];
+
+ if (!ComponentType) {
+ console.warn(`Component type "${component.type}" not found`);
+ return null;
+ }
+
+ // Extract children and render them recursively
+ const children = component.children?.map((child: JsxJson) => jsonToJsx(child, components, false)) || null;
+
+ // Build props from component properties
+ const props: any = { ...component };
+ delete props.children;
+ delete props.type;
+
+ // Add w-widget-scope class to root element
+ const baseClassName = props.className || '';
+ if (root) {
+ props.className = baseClassName ? `${baseClassName} w-widget-scope` : 'w-widget-scope';
+ }
+
+ // Handle text content
+ if (component.type === 'Text' || component.type === 'Title') {
+ return createElement(ComponentType, { ...props }, (component as any).value || children);
+ }
+
+ // Handle container components
+ return createElement(ComponentType, { ...props }, children);
+}
+
+/**
+ * Convert JSX string to React element
+ * Supports all widget components by default, can inject custom components and data
+ * @param {string} jsxStr - JSX String
+ * @param {Record} data - optional data objects in JSX , and then click the
+ * @param {Record} customComponents - Custom Component Map (optional)
+ * @returns {React.ReactElement}
+ */
+export function jsxStringToJsx(
+ jsxStr: string,
+ data?: Record,
+ customComponents?: Record>,
+ root = true,
+): React.ReactElement | null {
+ try {
+ // Merge custom components with default component map
+ const components = {
+ ...componentMap,
+ ...(customComponents || {}),
+ };
+
+ // 1. compile jsx to js using Babel
+ const compiled = Babel.transform(jsxStr, {
+ presets: ['react'],
+ }).code;
+
+ // 2. inject React + components scope + data
+ const argNames = ['React', ...Object.keys(components), ...(data ? Object.keys(data) : [])];
+ const argValues = [React, ...Object.values(components), ...(data ? Object.values(data) : [])];
+
+ // 3. execute code and return ReactElement
+ const fn = new Function(...argNames, `return ${compiled};`);
+ const result = fn(...argValues);
+
+ // 4. Add w-widget-scope class to root element
+ if (isValidElement(result)) {
+ const baseClassName = (result.props as any)?.className || '';
+ console.log('root', root, 'baseClassName', baseClassName);
+ const className = root ? (baseClassName ? `${baseClassName} w-widget-scope` : 'w-widget-scope') : baseClassName;
+ console.log('className', className);
+ return React.cloneElement(result, { className } as any);
+ }
+
+ return result;
+ } catch (error) {
+ const err = error as Error;
+ throw new Error(`Failed to convert JSX string: ${err.message}`);
+ }
+}
+
+export default jsonToJsx;
diff --git a/web/src/hooks/__tests__/useFilePreview.test.ts b/web/src/hooks/__tests__/useFilePreview.test.ts
new file mode 100644
index 0000000..2970733
--- /dev/null
+++ b/web/src/hooks/__tests__/useFilePreview.test.ts
@@ -0,0 +1,184 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { runFilePreview, tryInterceptAutoPreview, useFilePreview } from '../useFilePreview';
+import type { E2BFile } from '@/types';
+
+const mockFile: E2BFile = {
+ filename: 'test.pdf',
+ url: 'https://example.com/test.pdf',
+ content_type: 'application/pdf',
+ size: 1024,
+ path: '/test.pdf',
+};
+
+const createState = (overrides: Partial[0]> = {}) => {
+ const setFileViewerFile = vi.fn();
+ const setLastWorkspaceAction = vi.fn();
+
+ return {
+ layoutConfig: { narrowMode: false },
+ fullscreenFilePreview: false,
+ filePreviewConfig: undefined,
+ setFileViewerFile,
+ setLastWorkspaceAction,
+ ...overrides,
+ };
+};
+
+describe('runFilePreview', () => {
+ it('opens workspace preview in page mode', () => {
+ const state = createState();
+
+ const result = runFilePreview(state, {
+ file: mockFile,
+ siblings: [mockFile],
+ source: 'attachment',
+ });
+
+ expect(result).toEqual({ handled: false, mode: 'workspace' });
+ expect(state.setLastWorkspaceAction).toHaveBeenCalledWith('user');
+ expect(state.setFileViewerFile).toHaveBeenCalledWith(mockFile, [mockFile]);
+ });
+
+ it('returns modal mode in narrow layout without fullscreen', () => {
+ const state = createState({ layoutConfig: { narrowMode: true } });
+
+ const result = runFilePreview(state, {
+ file: mockFile,
+ siblings: [mockFile],
+ source: 'attachment',
+ });
+
+ expect(result).toEqual({ handled: false, mode: 'modal' });
+ expect(state.setFileViewerFile).not.toHaveBeenCalled();
+ });
+
+ it('opens workspace in narrow layout when fullscreenFilePreview is enabled', () => {
+ const state = createState({
+ layoutConfig: { narrowMode: true },
+ fullscreenFilePreview: true,
+ });
+
+ const result = runFilePreview(state, {
+ file: mockFile,
+ siblings: [mockFile],
+ source: 'attachment',
+ });
+
+ expect(result).toEqual({ handled: false, mode: 'workspace' });
+ expect(state.setFileViewerFile).toHaveBeenCalledWith(mockFile, [mockFile]);
+ });
+
+ it('only calls onPreview without built-in fallback when onPreview is provided', () => {
+ const onPreview = vi.fn();
+ const state = createState({
+ filePreviewConfig: { onPreview },
+ });
+
+ const result = runFilePreview(state, {
+ file: mockFile,
+ siblings: [mockFile],
+ source: 'attachment',
+ });
+
+ expect(result).toEqual({ handled: true });
+ expect(onPreview).toHaveBeenCalled();
+ expect(state.setFileViewerFile).not.toHaveBeenCalled();
+ });
+
+ it('calls onOpenModal via defaultPreview in narrow modal mode', () => {
+ const onOpenModal = vi.fn();
+ const onPreview = vi.fn(({ defaultPreview }) => {
+ defaultPreview();
+ });
+ const state = createState({
+ layoutConfig: { narrowMode: true },
+ filePreviewConfig: { onPreview },
+ });
+
+ runFilePreview(state, {
+ file: mockFile,
+ siblings: [mockFile],
+ source: 'attachment',
+ onOpenModal,
+ });
+
+ expect(onOpenModal).toHaveBeenCalled();
+ expect(state.setFileViewerFile).not.toHaveBeenCalled();
+ });
+});
+
+describe('tryInterceptAutoPreview', () => {
+ it('returns false when interceptAutoPreview is disabled', () => {
+ const state = createState({
+ filePreviewConfig: {
+ onPreview: vi.fn(() => true),
+ },
+ });
+
+ expect(tryInterceptAutoPreview(state, mockFile, [mockFile])).toBe(false);
+ expect(state.setFileViewerFile).not.toHaveBeenCalled();
+ });
+
+ it('only calls onPreview without auto fallback when interceptAutoPreview is enabled', () => {
+ const onPreview = vi.fn();
+ const state = createState({
+ filePreviewConfig: {
+ interceptAutoPreview: true,
+ onPreview,
+ },
+ });
+
+ expect(tryInterceptAutoPreview(state, mockFile, [mockFile])).toBe(true);
+ expect(onPreview).toHaveBeenCalledWith(
+ expect.objectContaining({ source: 'auto', file: mockFile, siblings: [mockFile] }),
+ );
+ expect(state.setFileViewerFile).not.toHaveBeenCalled();
+ });
+
+ it('opens workspace auto preview via defaultPreview when business opts in', () => {
+ const onPreview = vi.fn(({ defaultPreview }) => {
+ defaultPreview();
+ });
+ const state = createState({
+ filePreviewConfig: {
+ interceptAutoPreview: true,
+ onPreview,
+ },
+ });
+
+ expect(tryInterceptAutoPreview(state, mockFile, [mockFile])).toBe(true);
+ expect(state.setLastWorkspaceAction).toHaveBeenCalledWith('auto');
+ expect(state.setFileViewerFile).toHaveBeenCalledWith(mockFile, [mockFile]);
+ });
+});
+
+vi.mock('@/store', () => ({
+ useAgentStore: vi.fn(),
+}));
+
+import { useAgentStore } from '@/store';
+
+describe('useFilePreview', () => {
+ beforeEach(() => {
+ vi.mocked(useAgentStore).mockReturnValue(createState() as any);
+ });
+
+ it('delegates to runFilePreview via previewFile', () => {
+ const state = createState();
+ vi.mocked(useAgentStore).mockReturnValue(state as any);
+
+ const { result } = renderHook(() => useFilePreview());
+
+ act(() => {
+ const previewResult = result.current.previewFile({
+ file: mockFile,
+ siblings: [mockFile],
+ source: 'allFiles',
+ });
+ expect(previewResult).toEqual({ handled: false, mode: 'workspace' });
+ });
+
+ expect(state.setFileViewerFile).toHaveBeenCalledWith(mockFile, [mockFile]);
+ });
+});
diff --git a/web/src/hooks/__tests__/useUrlContent.test.ts b/web/src/hooks/__tests__/useUrlContent.test.ts
new file mode 100644
index 0000000..356e087
--- /dev/null
+++ b/web/src/hooks/__tests__/useUrlContent.test.ts
@@ -0,0 +1,45 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { useUrlContent } from '../useUrlContent';
+
+describe('useUrlContent', () => {
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+ });
+
+ it('creates a fresh object URL when a cached PDF is opened again', async () => {
+ const url = 'https://example.com/cached-preview.pdf';
+ const pdfBytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]).buffer;
+ const fetchMock = vi.fn().mockResolvedValue({
+ ok: true,
+ headers: {
+ get: vi.fn().mockReturnValue('application/pdf'),
+ },
+ arrayBuffer: vi.fn().mockResolvedValue(pdfBytes),
+ });
+ const createObjectURL = vi
+ .fn()
+ .mockReturnValueOnce('blob:https://example.com/first')
+ .mockReturnValueOnce('blob:https://example.com/second');
+
+ vi.stubGlobal('fetch', fetchMock);
+ vi.stubGlobal('URL', { createObjectURL });
+
+ const firstPreview = renderHook(() => useUrlContent({ url }));
+
+ await waitFor(() => {
+ expect(firstPreview.result.current.blobUrl).toBe('blob:https://example.com/first');
+ });
+ firstPreview.unmount();
+
+ const secondPreview = renderHook(() => useUrlContent({ url }));
+
+ await waitFor(() => {
+ expect(secondPreview.result.current.blobUrl).toBe('blob:https://example.com/second');
+ });
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ expect(createObjectURL).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/web/src/hooks/useActionExecutor.ts b/web/src/hooks/useActionExecutor.ts
new file mode 100644
index 0000000..16c32d1
--- /dev/null
+++ b/web/src/hooks/useActionExecutor.ts
@@ -0,0 +1,196 @@
+import { useCallback, useContext, useEffect, useRef, useState } from 'react';
+import type { SyntheticEvent } from 'react';
+
+import eventBus from '@/utils/eventBus';
+import type { ActionConfig } from '@/components/widgets/types';
+import { executeClientAction } from '@/sdk/clientActions';
+import { WidgetRenderContext } from '@/components/WidgetRender';
+import { collectFormData } from './useFormDataCollector';
+import { useAgentStore } from '@/store';
+
+export interface UseActionExecutorOptions {
+ /**
+ * Fields injected into every action.payload by default.
+ * Commonly used for context such as the widget type or field name that triggered the action.
+ */
+ defaultPayload?: Record;
+
+ /**
+ * Whether to collect form data automatically.
+ * @default true - Collect form data from the nearest widget scope and merge it into the payload.
+ */
+ collectFormData?: boolean;
+
+ /**
+ * Fields injected into every action.payload by default.
+ * Commonly used for context such as the widget type or field name that triggered the action.
+ */
+ label?: string;
+}
+
+export interface ExecuteActionOptions {
+ /**
+ * Payload supplied at call time. It is merged with the default payload, form data,
+ * and the payload defined by the action itself.
+ */
+ payload?: Record;
+ /**
+ * Raw event object passed through to the client handler.
+ */
+ event?: SyntheticEvent | Event | null;
+ /**
+ * Whether to skip form-data collection.
+ * @default false
+ */
+ skipFormDataCollection?: boolean;
+}
+
+const nextFrame = () =>
+ new Promise((resolve) => {
+ if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
+ window.requestAnimationFrame(() => resolve());
+ return;
+ }
+ setTimeout(() => resolve(), 16);
+ });
+
+const shouldHandleLoading = (action?: ActionConfig) => {
+ if (!action) return false;
+ if (!action.loadingBehavior || action.loadingBehavior === 'auto') return true;
+ return action.loadingBehavior !== 'none';
+};
+
+/**
+ * Deeply merge objects while preserving nested properties.
+ */
+const deepMerge = (target: Record, ...sources: Record[]): Record => {
+ if (!sources.length) return target;
+ const source = sources.shift();
+
+ if (!source) return deepMerge(target, ...sources);
+
+ for (const key in source) {
+ const targetValue = target[key];
+ const sourceValue = source[key];
+
+ if (
+ sourceValue &&
+ typeof sourceValue === 'object' &&
+ !Array.isArray(sourceValue) &&
+ targetValue &&
+ typeof targetValue === 'object' &&
+ !Array.isArray(targetValue)
+ ) {
+ target[key] = deepMerge({ ...targetValue } as Record, sourceValue as Record);
+ } else {
+ target[key] = sourceValue;
+ }
+ }
+
+ return deepMerge(target, ...sources);
+};
+
+export const useActionExecutor = (options?: UseActionExecutorOptions) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const defaultPayloadRef = useRef>(options?.defaultPayload ?? {});
+ const collectFormDataFlag = useRef(options?.collectFormData ?? true);
+ const { isPreviewMode } = useContext(WidgetRenderContext);
+ const instanceId = useAgentStore((s) => s.instanceId);
+
+ useEffect(() => {
+ defaultPayloadRef.current = options?.defaultPayload ?? {};
+ collectFormDataFlag.current = options?.collectFormData ?? true;
+ }, [options?.defaultPayload, options?.collectFormData]);
+
+ const executeAction = useCallback(
+ async (action?: ActionConfig, execOptions?: ExecuteActionOptions) => {
+ if (!action) {
+ return;
+ }
+
+ console.log('executeAction', action, execOptions);
+
+ // Preview mode: block action execution
+ if (isPreviewMode) {
+ console.log('[Preview Mode] Action blocked:', {
+ type: action.type,
+ handler: action.handler ?? 'server',
+ payload: action.payload,
+ });
+ return;
+ }
+
+ const manageLoading = shouldHandleLoading(action);
+ if (manageLoading) {
+ setIsLoading(true);
+ }
+
+ // Collect form data.
+ let formData: Record = {};
+ if (collectFormDataFlag.current && !execOptions?.skipFormDataCollection && execOptions?.event) {
+ const target = (execOptions.event as any).target || (execOptions.event as any).currentTarget;
+ if (target instanceof HTMLElement) {
+ // Find the nearest widget scope.
+ const widgetScope = target.closest('.w-widget-scope');
+ if (widgetScope instanceof HTMLElement) {
+ formData = collectFormData(widgetScope);
+ console.log('Collected form data:', formData);
+ }
+ }
+ }
+
+ // Merge precedence: action.payload < defaultPayload < formData < execOptions.payload.
+ // Deep merging preserves nested object values.
+ const mergedPayload: Record = deepMerge(
+ {},
+ action.payload ?? {},
+ defaultPayloadRef.current,
+ formData, // Merge form fields directly into the payload instead of nesting them under formData.
+ execOptions?.payload ?? {},
+ );
+
+ const params = {
+ type: action.type,
+ payload: mergedPayload,
+ label: options?.label,
+ };
+
+ console.log('📤 Action to be sent:', params);
+
+ try {
+ if ((action.handler ?? 'server') === 'client') {
+ await executeClientAction({
+ action: action.type,
+ payload: params,
+ event: execOptions?.event ?? null,
+ });
+ return;
+ }
+
+ eventBus.emit(`call_send_${instanceId}`, {
+ type: 'custom_action',
+ params: {
+ action: params,
+ },
+ });
+
+ // Wait for the next frame so the loading state is rendered at least once.
+ if (manageLoading) {
+ await nextFrame();
+ }
+ } finally {
+ if (manageLoading) {
+ setIsLoading(false);
+ }
+ }
+ },
+ [instanceId, isPreviewMode, options?.label],
+ );
+
+ return {
+ executeAction,
+ isLoading,
+ };
+};
+
+export default useActionExecutor;
diff --git a/web/src/hooks/useChat/DEBUG_SEND_CONTINUE.md b/web/src/hooks/useChat/DEBUG_SEND_CONTINUE.md
new file mode 100644
index 0000000..d67f022
--- /dev/null
+++ b/web/src/hooks/useChat/DEBUG_SEND_CONTINUE.md
@@ -0,0 +1,52 @@
+# Debugging Unexpected `send-continue` Requests
+
+`send-continue` resumes a session after an interrupted stream. An unexpected request usually means that one of the recovery paths considered the last assistant message unfinished.
+
+## Call Paths
+
+The production call sites are:
+
+- `core/useSend.ts`: resumes after a send response closes without a terminal message.
+- `core/useSendContinue.ts`: retries a failed or incomplete continuation.
+- `network/useNetworkRecovery.ts`: resumes after connectivity, focus, visibility, or bfcache recovery.
+
+`core/useSSEHandler.ts` detects stream completion and invokes the retry callback supplied by these hooks.
+
+## Diagnostic Sequence
+
+1. Locate the `[sendContinue]` log and inspect its stack trace, retry count, timestamp, and `chunk_id`.
+2. Inspect the last non-synthetic chunk. A completed assistant response must include the expected terminal state.
+3. Confirm that only one recovery source fired. Online, focus, visibility, and `pageshow` events are debounced, but multiple hook instances can still indicate an ownership problem.
+4. Check whether a previous retry timer survived a session change or component unmount.
+5. Confirm that `AbortError` is treated as an intentional interruption and that offline recovery waits for the online event.
+6. Verify that `client_tool_result` delivery does not remove or replace the assistant chunk used as the continuation anchor.
+
+## Useful Breakpoints
+
+Set breakpoints at each `sendContinue()` call and in:
+
+- `useSendContinue` before filtering synthetic chunks
+- the SSE completion callback
+- the maximum-retry handler
+- `useNetworkRecovery` before its debounce timer is created
+- session cleanup where retry timers and `AbortController` instances are cleared
+
+## Common Causes
+
+- The stream closed without a `finish_reason` or equivalent terminal chunk.
+- A stale hook instance still owns a timer.
+- The active session changed while a client tool was running.
+- A visibility or focus event resumed a session that had already completed.
+- Synthetic status chunks obscured the last real assistant chunk.
+
+## Verification
+
+Run the focused suites before the full test suite:
+
+```bash
+pnpm test --run src/hooks/useChat/core/__tests__/useSendContinue.test.ts
+pnpm test --run src/hooks/useChat/network/__tests__/useNetworkRecovery.test.ts
+pnpm test --run
+```
+
+Validate both successful continuation and the maximum-retry path. Confirm that session changes and unmounts clear timers and abort active requests.
diff --git a/web/src/hooks/useChat/__tests__/mock.ts b/web/src/hooks/useChat/__tests__/mock.ts
new file mode 100644
index 0000000..83484fa
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/mock.ts
@@ -0,0 +1,2354 @@
+import { InnerMessageChunk } from '@/types';
+
+export const mockData = [
+ {
+ // User Message
+ // Messageid
+ id: 'chunk_1748438204041_0',
+ // Message Type
+ type: 'text',
+ // Message Role
+ role: 'user',
+ // Message entered by user
+ content: 'Free park near Nakawan Village.',
+ config: {
+ // The knowledge base cited
+ knowledgeIds: [1, 2, 3],
+ // QuotedMCP、tool、agentWait.
+ tools: [1, 2, 3],
+ // Uploading files
+ files: [
+ {
+ name: 'Documentation1',
+ type: 'pdf',
+ url: '', // Upload
+ },
+ ],
+ },
+ timestamp: 1748438204041,
+ },
+ {
+ // AIMessage
+ id: 'chunk_1748438204041_1',
+ type: 'text',
+ role: 'assistant',
+ content: 'I\'ll find out what\'s in the free park near Central Guan Village.',
+ timestamp: 1748438204041,
+ },
+ {
+ // Start thinking.
+ id: '123123123123123',
+ type: 'liveStatus',
+ content: 'Thinking.',
+ timestamp: 1748438204041,
+ },
+ {
+ // Start planning.
+ id: 'liveStatus1',
+ type: 'liveStatus',
+ content: 'Planning',
+ timestamp: 1748438204041,
+ },
+ {
+ // Generating Planning
+ id: 'chunk_1748438204041_3',
+ type: 'plan',
+ content: 'AI Planning',
+ timestamp: 1748438204041,
+ detail: {
+ steps: [
+ {
+ id: 'step1',
+ title: 'Search free park near Central Guan Village.',
+ description: 'Steps1Description',
+ status: 'running',
+ started_at: 1748438204041,
+ },
+ {
+ id: 'step2',
+ title: 'Validation of park information and location',
+ description: 'Steps2Description',
+ status: 'pending',
+ started_at: 1748438204041,
+ },
+ {
+ id: 'step3',
+ title: 'You can sort out the park details and document them.',
+ description: 'Steps3Description',
+ status: 'pending',
+ started_at: 1748438204041,
+ },
+ {
+ id: 'step4',
+ title: 'Feedback and send the document to the user',
+ description: 'Steps4Description',
+ status: 'pending',
+ started_at: 1748438204041,
+ },
+ ],
+ },
+ },
+ {
+ // The first step in thinking.
+ id: 'qYOmpYISgzC2ziKnRuTEsY',
+ type: 'liveStatus',
+ timestamp: 1748438205657,
+ content: 'Thinking.',
+ step_id: 'step1',
+ },
+ {
+ // I thought about it again.
+ id: 'HON4z3RCHU00hvqjyDmxaB',
+ type: 'liveStatus',
+ timestamp: 1748438205703,
+ content: 'Thinking.',
+ step_id: 'step1',
+ },
+ {
+ // Still thinking.
+ id: 'KRrgXUoXE9S79Wd0OFohNU',
+ type: 'liveStatus',
+ timestamp: 1748438207333,
+ content: 'Thinking.',
+ step_id: 'step1',
+ },
+ {
+ // Output line word, start searching
+ id: 'chunk_1748438204041_5',
+ type: 'text',
+ content: 'Start searching for free park information near Central Guan Village.',
+ timestamp: 1748438204041,
+ step_id: 'step1',
+ },
+ {
+ // Call the tool. Start searching.
+ id: 'chunk_1748438204041_6',
+ type: 'tool_call', // I'll replace it with one inside. tool Name
+ content: 'Searching',
+ detail: {
+ tool: 'search', // Tooltype identifier
+ action: 'Searching',
+ param: 'Free park near Nakawan Village.',
+ status: 'pending', // Status: 'pending' | 'running' | 'success' | 'error'
+ },
+ timestamp: 1748438204041,
+ step_id: 'step1',
+ },
+ {
+ // Tool Call Results
+ id: 'chunk_1748438204041_7',
+ type: 'tool_result',
+ content: 'Searching',
+ detail: {
+ tool: 'search',
+ action: 'Search Results',
+ param: 'Free park near Nakawan Village.',
+ result: [
+ {
+ favicon: 'TODO',
+ link: 'https://m.bj.bendibao.com/tour/143161_5.html',
+ description:
+ 'Unlike other urban parks, Central Guanamura Forest Parks are built“Near natural forest”Targeted, forest area covers the entire park70%The plants are planted in the following species:100Multiple, mixed with Joe\'s weed, pint trees, broad leaves, different. ...',
+ title: 'Beijing is free to go to the sea of the land.:Tang Jia Ling turned into a forest park in the village',
+ },
+ {
+ favicon: 'TODO',
+ icon: 'https://fanyi.baidu.com/favicon.ico',
+ link: 'https://www.visitbeijing.com.cn/article/48WUPIgkf4s',
+ description:
+ 'The water park of the Elms is a blue green landscape that links the entire line to slow-walking systems. ...',
+ title: 'The seaside is full of water, ecology, free water, and the north is filled with large parks. - Beijing Tourism Network',
+ },
+ {
+ favicon: 'TODO',
+ icon: 'https://mdn.alipayobjects.com/huamei_iwk9zp/afts/img/A*eco6RrQhxbMAAAAAAAAAAAAADgCCAQ/original',
+ link: 'https://zhuanlan.zhihu.com/p/41983280',
+ description:
+ 'The park is a willow tree, which is famous for its shades in summer. It\'s a place where you can see the feeling of crossing, the hymns, the sex of the love of the mountain..',
+ title: 'I\'m going to Beijing.18Free park, recommended! - I know the column.',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://m.bj.bendibao.com/tour/240734.html',
+ description:
+ 'Beijing Free Park.13A free park recommendation. ; 1、Olympic Forest Park ; Route by car: ; 2、Seatside Park. ; Address: New Palace Gate Road in the Seatside Area2No, no, no. ; Route of transport:.',
+ title: 'Beijing Free Park.13A free park recommendation. - Beijing local treasure.',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.sohu.com/a/272848567_100143624',
+ description:
+ 'The Chinese forest park is located on Tang Jia Ling Road, and our friend who drove himself to the park came to the park by a quiet road, stopped for free and without a ticket, and was small, wide and quiet. ...',
+ title: 'Beijing is free, and the sea is full of fun._Park - Search the fox.',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://s.visitbeijing.com.cn/attraction/120931',
+ description:
+ 'The North Space City, South-South Connecting Software Park, East Iking New Highway is the focus of the construction of the reforestation project in the Seatside plain this year.2940The park is built over time.3The year. ...',
+ title: 'Central Guanamura Forest Park - Beijing Tourism Network',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.beijing.gov.cn/renwen/sy/whkb/201905/t20190530_1866177.html',
+ description:
+ 'Free park at home.11The site is open.. This park we\'re talking about.. It\'s at the border between Toda, Haydam, West Side.. They\'re used by the locals to their own garden.. Lotus Pool Park.. Come and get your card! ...',
+ title: 'Come on, hit the cards! Free park at home.11All the viewers open. - Beijing City People \' s Government',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://hk.trip.com/moments/detail/beijing-1-128796312/',
+ description:
+ 'The best season is at the firecracker\'s Park, free of charge.. The Yubaku Park, South Avenue, in the middle of the Haidian district of Beijing.35No. It\'s known for the "Full-Sun-Take-Take" temple in the northwest of the park, which is the largest land mass.457300 ...',
+ title: 'The best season is to go to the firecracker park, free of charge.Trip.com Beijing',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.sohu.com/a/201115214_161623',
+ description:
+ 'In recent years, especially, according to urban planning, a Chinese-Kanji village forest park has been built on the old site of Tang Jia Ling Village, which is open to citizens free of charge, a north-west space park, a south-west software park, and a new highway in East Iking. ...',
+ title: 'New view of Central Guanamura Forest Park city - Search the fox.',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://baike.baidu.com/item/%E7%B4%AB%E7%AB%B9%E9%99%A2%E5%85%AC%E5%9B%AD/3312060',
+ description:
+ 'The Zaiwan Park, on South Avenue, Central Province, Beijing35No. There\'s a temple in the northwest of the park.“The Zhuang-Bok-Bok-Bok.”And the name is the total area of the land.457300The Zippet Park is built in square metres.1953The year.2006Year7Month1Free of charge from day ...',
+ title: 'The Zakeh Park._Pepsi.',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://cn.tripadvisor.com/Attraction_Review-g294212-d1793435-Reviews-Haidian_Park-Beijing.html',
+ description:
+ 'The name of the park is also made possible by its unique geographical location and its size.40hectares, where greened area is reached30The park is a green eco-park.10It\'s all over the landscape. ...',
+ title: 'Seatside Park.(Beijing) - Tourist site review - Tripadvisor',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.beijing.gov.cn/renwen/jrbj/csjz/201908/t20190815_1874388.html',
+ description:
+ 'There\'s a park on the South Ribbon in Beijing.…… Beijing\'s first theme park centred on old age activities. This park, it\'s Mansion Park.. Mansai Park is located at the Kandai Temple, built by Ming Dynasty..',
+ title: 'This free park in the ring is the best place for old people to feed! - Beijing City People \' s Government',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://zhidao.baidu.com/question/11033050?bd_page_type=0&pu=&init=',
+ description:
+ 'It is one of the landmarks of Beijing University. Olympic Park: Beijing Olympic Park is located in the Zhongyang district of Beijing City, north of Beijing City, north to the south bank of the Chiang Hae, south to the north to the earth, east to Anli and north to east, west to Lim Twe and north to west ...',
+ title: 'Is there a free park near Nakawan Village? - - I know.',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://pkugv.pku.edu.cn/shzn/zbjd/index.htm',
+ description:
+ '... I\'m free of charge at the old age-based visitors\' park.(No parks, except during large events),Disabled persons are exempt from the ticket, address: Qinghua Si Road, Haidian District, Beijing28No, no, no.. Route of transport: Imbot4、Transport105、Transport205、365、432 ...',
+ title: 'Life Guide - Beijing University, Guan Xin Park',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://hk.trip.com/moments/detail/beijing-1-130713360/',
+ description:
+ 'The scenery feature: Free park, a lot of people dancing, a nice view. ... Othertips: The Yubaku Park, South Avenue, in the middle of the Haidian district of Beijing.35 No. Because there\'s a temple in the northwest of the park, the "Full-Shot-Take-Take-Take-Take-Take-Take" ...',
+ title: 'Beijing\'s free country trip to Spring Park. - Trip.com',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://cn.tripadvisor.com/AttractionsNear-g294212-d1793435-Haidian_Park-Beijing.html',
+ description:
+ 'Seatside Park.. 4.3. (36 Punctuation). 100080 Northwest corner of the bridge at the Wanquan River in the northwest of the Haidian District, Beijing, China ; The Seadow Museum.. 5.0. (1 Punctuation). 100080 New Miyagate Road, Haidang District, Beijing, China2No, no, no. ; Beijing University.',
+ title: 'Near Seatown Park.10 Have some fun with the big view. - Tripadvisor',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.expedia.com/cn/Beijing-Zhongguancun.dx6160223',
+ description:
+ 'Hot spot. ; Wang Ji Street. 4.5/5(9 Punctuation) ; China\'s Great Wall ; Forbidden City.(The palace.). 4/5(15 Punctuation) ; Tiananmen Square. 5/5(9 Punctuation) ; - Three Mile Road.. 5/5(2 Punctuation).',
+ title: 'To Central Guan village: Beijing tour in Nakawan village2025 | Expedia Travel',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://bj.zu.anjuke.com/fangyuan/4080379961826316',
+ description:
+ 'There are many parks and cultural squares nearby, such as the Zhichun Park.288Mehri, Elm Park (Photobre)451The Golden Five Sports Park. ...',
+ title: 'I\'m at the Hokkai Park Park.3Line - Beijing rental',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.douyin.com/search/%E4%B8%AD%E5%85%B3%E6%9D%91%E5%85%AC%E5%9B%AD%E5%8D%97%E9%97%A8%E5%81%9C%E8%BD%A6%E5%9C%BA%E5%85%8D%E8%B4%B9',
+ description:
+ 'Beijing, China, Guan village forest park.24Hours open.|Free.PIWhat about the free parking park at the South Gate and the South Gate?? Parks are divided into East and West, and the East is much more fun, and the villages are in the navigational forest. ...',
+ title: 'Free parking at the South Gate of Central Guangang Park. - Dive',
+ },
+ {
+ favicon: 'TODO',
+ link: 'https://www.booking.com/landmark/cn/fragrant-hills-park.zh-cn.html',
+ description:
+ 'The Chinese Science and Technology Park, Beijing, has chosen the Holidays Hotel.l Next to the rounding park &Beijing Sports University&Agricultural UniversitylTaxi!10I\'ll be at the Zhuan Garden in a minute.&Great Qinghua NorthlHigh iron20The Great Wall of the 8th Ridge is located in Beijing, not far from the Garden.6.9Kilometres, provide quick processing ...',
+ title: 'Hotel near the Beijing Shanshan Park.',
+ },
+ ],
+ status: 'success',
+ },
+ timestamp: 1748438204041,
+ step_id: 'step1',
+ },
+ {
+ // Output one line of words to prepare for verification and collation of details
+ id: 'gdlOAbH1qqb9KLnrDMD1kZ',
+ type: 'text',
+ content: 'Search results were obtained from free park near Central Guanamura, and detailed information was prepared for verification and collation',
+ timestamp: 1748438207348,
+ step_id: 'step1',
+ },
+ {
+ // Update the plan
+ id: 'chunk_1748438204041_8',
+ type: 'plan_update',
+ content: 'Update on the plan',
+ detail: {
+ action: 'update', // add/update/remove
+ steps: [
+ {
+ id: 'step1',
+ title: 'Search free park near Central Guan Village.',
+ description: 'Steps1Description',
+ // The first one is finished.
+ status: 'success',
+ started_at: 1748438204041,
+ },
+ {
+ id: 'step2',
+ title: 'Validation of park information and location',
+ description: 'Steps2Description',
+ // Article 2 begins.
+ status: 'running',
+ started_at: 1748438204041,
+ },
+ ],
+ },
+ timestamp: 1748438204041,
+ },
+ {
+ // The second step is thinking.
+ id: 'liveStatus3',
+ type: 'liveStatus',
+ content: 'AI I\'m thinking.',
+ timestamp: 1748438204041,
+ step_id: 'step2',
+ },
+ {
+ // The action in the second step.
+ id: 'MeTsHrtrM9OcQeU7t9PnlU',
+ type: 'liveStatus',
+ timestamp: 1748438209327,
+ content: 'Use Browser',
+ step_id: 'step2',
+ },
+ {
+ // The action in the second step.
+ id: 'BpatldBko31ESGqtivbVkj',
+ type: 'liveStatus',
+ timestamp: 1748438209327,
+ content: 'Initializing computer',
+ step_id: 'step2',
+ },
+ {
+ // Call tools, use browser
+ id: 'chunk_1748438204041_10',
+ type: 'tool_call',
+ content: 'Using Browser',
+ detail: {
+ tool: 'browser',
+ action: 'Using Browser',
+ param: 'https://m.bj.bendibao.com/tour/143161_5.html',
+ status: 'pending',
+ },
+ step_id: 'step2',
+ },
+ {
+ id: 'pNDaNYPYx2itFGImtBdxHD',
+ type: 'tool_result',
+ content: 'Using Browser',
+ detail: {
+ tool: 'browser',
+ action: 'Using Browser',
+ param: 'https://m.bj.bendibao.com/tour/143161_5.html',
+ result: {
+ screenshot:
+ 'https://resource.example.invalid/screenshots/SIGNED_URL_EXAMPLE',
+ },
+ status: 'success',
+ },
+ step_id: 'step2',
+ },
+ {
+ id: 'zlyFOkxYLOzRRLCeF6nrKu',
+ type: 'liveStatus',
+ timestamp: 1748438216803,
+ content: 'View Browser',
+ step_id: 'step2',
+ },
+ {
+ id: '4u2ZCA057YPQHLE2eWbgwH',
+ type: 'liveStatus',
+ timestamp: 1750073433215,
+ content: 'Use Terminal',
+ step_id: 'step2',
+ },
+ {
+ // Call tools, use browser
+ id: 'azRKKVXFICJYVceLBKFrGu',
+ type: 'tool_call',
+ content: 'Using Terminal',
+ detail: {
+ tool: 'terminal',
+ action: 'Commands are in progress',
+ param:
+ "python3 -c \"\nimport re\n\ndef ansi_to_html(ansi_string):\n html_string = ansi_string\n # Green color\n html_string = re.sub(r'\\u001b\\[32m', '', html_string)\n # Reset color\n html_string = re.sub(r'\\u001b\\[0m', ' ', html_string)\n # Replace newlines with \n html_string = html_string.replace('\\n', ' ')\n return html_string\n\nansi_input = \\\"\\u001b[32mubuntu@sandbox:~ $\\u001b[0m cd /home/ubuntu && mkdir -p chartmetric_analysis && cd chartmetric_analysis && mkdir -p data\\n\\n\\u001b[32mubuntu@sandbox:~/chartmetric_analysis $\\u001b[0m\\\"\nhtml_output = ansi_to_html(ansi_input)\n\nwith open('output.html_content', 'w') as f:\n f.write(html_output)\n\nprint(html_output)\n\"",
+ status: 'pending',
+ },
+ step_id: 'step2',
+ },
+ {
+ id: '6VdmLj9imJKUfZF60FEBGK',
+ type: 'tool_result',
+ content: 'Using Terminal',
+ detail: {
+ tool: 'terminal',
+ action: 'Commands are in progress',
+ param:
+ "python3 -c \"\nimport re\n\ndef ansi_to_html(ansi_string):\n html_string = ansi_string\n # Green color\n html_string = re.sub(r'\\u001b\\[32m', '', html_string)\n # Reset color\n html_string = re.sub(r'\\u001b\\[0m', ' ', html_string)\n # Replace newlines with \n html_string = html_string.replace('\\n', ' ')\n return html_string\n\nansi_input = \\\"\\u001b[32mubuntu@sandbox:~ $\\u001b[0m cd /home/ubuntu && mkdir -p chartmetric_analysis && cd chartmetric_analysis && mkdir -p data\\n\\n\\u001b[32mubuntu@sandbox:~/chartmetric_analysis $\\u001b[0m\\\"\nhtml_output = ansi_to_html(ansi_input)\n\nwith open('output.html_content', 'w') as f:\n f.write(html_output)\n\nprint(html_output)\n\"",
+ result: {
+ output:
+ "\u001b[32mubuntu@sandbox:~ $\u001b[0m cd /home/ubuntu/chartmetric_analysis && python3 -c \"\nimport re\n\ndef ansi_to_html(ansi_string):\n html_string = ansi_string\n # Green color\n html_string = re.sub(r'\\u001b\\[32m', '', html_string)\n # Reset color\n html_string = re.sub(r'\\u001b\\[0m', ' ', html_string)\n # Replace newlines with \n html_string = html_string.replace('\\n', ' ')\n return html_string\n\nansi_input = \\\"\n\u001b[32mubuntu@sandbox:~ $\u001b[0m cd /home/ubuntu && mkdir -p chartmetric_analysis && cd chartmetric_analysis && mkdir -p data\\n\\n\n\u001b[32mubuntu@sandbox:\u001b[0m ~/chartmetric_analysis $\\\"\nhtml_output = ansi_to_html(ansi_input)\n\nwith open('output.html_content', 'w') as f:\n f.write(html_output)\n\nprint(html_output)\n\"\nbash: cd: /home/ubuntu/chartmetric_analysis: No such file or directory\n\u001b[32mubuntu@sandbox:~ $\u001b[0m",
+ },
+ status: 'success',
+ },
+ step_id: 'step2',
+ },
+ {
+ id: 'WmCM53z9dtQzYQ598XatBc',
+ type: 'liveStatus',
+ timestamp: 1748438221172,
+ content: 'Update the plan',
+ step_id: 'step2',
+ },
+ {
+ id: '2TOZBDhfPWANAXsR1AacGE',
+ type: 'plan_update',
+ content: 'Update on the plan',
+ detail: {
+ action: 'update', // add/update/remove
+ steps: [
+ {
+ id: 'step2',
+ title: 'Validation of park information and location',
+ description: 'Steps2Description',
+ status: 'success',
+ started_at: 1748438204041,
+ },
+ {
+ id: 'step3',
+ title: 'You can sort out the park details and document them.',
+ description: 'Steps3Description',
+ status: 'running',
+ started_at: 1748438204041,
+ },
+ ],
+ },
+ timestamp: 1748438221172,
+ },
+ {
+ id: 'QWXlAjDX3qevDfNWkKr2d5',
+ type: 'liveStatus',
+ timestamp: 1748438269328,
+ content: 'Use Terminal',
+ step_id: 'step3',
+ },
+ {
+ id: 'Kyiz0OoxoY7PORLfi9c0nn',
+ type: 'liveStatus',
+ timestamp: 1748438272558,
+ content: 'Edit File zhongguancun_parks/todo.md',
+ step_id: 'step3',
+ },
+ {
+ id: 'lS6xqH24ABeLKSv2U6SlFg',
+ type: 'tool_call',
+ content: 'Creating file `zhongguancun_parks/todo.md`',
+ detail: {
+ tool: 'code',
+ action: 'Creating file',
+ param: `zhongguancun_parks/todo.md`,
+ status: 'pending',
+ },
+ step_id: 'step3',
+ },
+ {
+ id: 'chunk_1748438204041_11',
+ type: 'tool_result',
+ content: 'Creating file',
+ detail: {
+ tool: 'code',
+ action: 'Creating file',
+ param: `zhongguancun_parks/todo.md`,
+ file: 'zhongguancun_parks/todo.md',
+ language: 'markdown',
+ content:
+ '# Free Park Information Processing near Nakawan Village\n\n- [x] Search for free park information near the village of Guan.\n- [x] Verification of the free policy and geographical location of the park\n- [ ] We\'ll sort out the details of Central Guan Village Forest Park.\n- [ ] We\'ll sort out the plankton park.\n- [ ] We\'ll sort out the Haydock Park details.\n- [ ] We\'ll sort out other free park information.\n- [ ] Summarize all information into a complete document\n- [ ] Check for completeness and accuracy of document contents\n- [ ] Send final document to user\n',
+ status: 'success',
+ },
+ step_id: 'step3',
+ },
+ {
+ id: 'pdf1',
+ type: 'tool_call',
+ content: 'Creating a file for free park information-processing near Central Guan Village.pdf',
+ detail: {
+ tool: 'file',
+ action: 'Creating file',
+ param: 'Free park information in Central Guan Village..pdf',
+ status: 'pending',
+ },
+ step_id: 'step3',
+ },
+ {
+ id: 'pdf2',
+ type: 'tool_result',
+ content: 'Creating file',
+ detail: {
+ tool: 'file',
+ action: 'Creating file',
+ param: 'Free park information in Central Guan Village..pdf',
+ name: 'Free park information in Central Guan Village..pdf',
+ url: 'https://arxiv.org/pdf/2506.15664?page=6',
+ status: 'success',
+ },
+ step_id: 'step3',
+ },
+ {
+ id: '2lhGlOxoO6orsUeAZiMuBp',
+ type: 'liveStatus',
+ timestamp: 1748438277241,
+ content: 'Update the plan',
+ step_id: 'step3',
+ },
+ {
+ id: 'chunk_1748438204041_12',
+ type: 'plan_update',
+ content: 'Update on the plan',
+ detail: {
+ action: 'update', // add/update/remove
+ steps: [
+ {
+ id: 'step3',
+ title: 'You can sort out the park details and document them.',
+ description: 'Steps3Description',
+ status: 'success',
+ },
+ {
+ id: 'step4',
+ title: 'Feedback and send the document to the user',
+ description: 'Steps4Description',
+ status: 'running',
+ },
+ ],
+ },
+ },
+ {
+ id: 'jTWyxQnGWb13oZURpTgvSi',
+ type: 'liveStatus',
+ timestamp: 1748438439005,
+ content: 'All jobs completed, final documents and all associated files are being sent to users',
+ step_id: 'step4',
+ },
+ {
+ id: 'f3AQYxP9n4j3wUgUy3Gjxa',
+ type: 'text',
+ content: 'Completed',
+ attachments: [
+ {
+ uid: '1',
+ name: 'excel-file.xlsx',
+ size: 111111,
+ },
+ {
+ uid: '2',
+ name: 'word-file.docx',
+ size: 222222,
+ },
+ {
+ uid: '3',
+ name: 'image-file.png',
+ size: 333333,
+ },
+ {
+ uid: '4',
+ name: 'pdf-file.pdf',
+ size: 444444,
+ },
+ {
+ uid: '5',
+ name: 'ppt-file.pptx',
+ size: 555555,
+ },
+ {
+ uid: '6',
+ name: 'video-file.mp4',
+ size: 666666,
+ },
+ {
+ uid: '7',
+ name: 'audio-file.mp3',
+ size: 777777,
+ },
+ {
+ uid: '8',
+ name: 'zip-file.zip',
+ size: 888888,
+ },
+ {
+ uid: '9',
+ name: 'markdown-file.md',
+ size: 999999,
+ description: 'Custom description here',
+ },
+ {
+ uid: '10',
+ name: 'image-file.png',
+ thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+ url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
+ size: 123456,
+ },
+ ],
+ },
+ {
+ id: 'tJ0BmayO3CgqyBVxqR0IyR',
+ type: 'plan_update',
+ content: 'Update on the plan',
+ detail: {
+ action: 'update', // add/update/remove
+ steps: [
+ {
+ id: 'step4',
+ title: 'Feedback and send the document to the user',
+ description: 'Steps4Description',
+ status: 'success',
+ },
+ ],
+ },
+ timestamp: 1748438439005,
+ },
+];
+
+export const mockPhoneStartApp = [
+ {
+ id: '1752139298251_0ac8adff',
+ role: 'assistant',
+ type: 'tool_call',
+ content: 'phone_start_app',
+ detail: {
+ tool: 'phone_start_app',
+ param: {
+ package: 'com.android.calculator2',
+ },
+ status: 'pending',
+ output: {},
+ run_id: '3a0e5e9a-bf55-4bb1-bf8d-bd7a19a9dd0d',
+ },
+ timestamp: 1752193298302,
+ },
+ {
+ id: '1752139304579_b39be6a0',
+ role: 'assistant',
+ type: 'tool_result',
+ content:
+ '{"result":"Started com.android.calculator2", "current_state":{"clickable_elements":[{"text":"More Options", "className":"ImageButton", "index":0, "bounds":"640,56,720,152", "resourceId":"", "type":"clickable", "isParent":true}, {"text":"digit_7", "className":"Button", "index":2, "bounds":"16,476,173,667", "resourceId":"com.android.calculator2:id/digit_7", "type":"clickable", "isParent":true}, {"text":"digit_8", "className":"Button", "index":3, "bounds":"173,476,331,667", "resourceId":"com.android.calculator2:id/digit_8", "type":"clickable", "isParent":true}, {"text":"digit_9", "className":"Button", "index":4, "bounds":"331,476,488,667", "resourceId":"com.android.calculator2:id/digit_9", "type":"clickable", "isParent":true}, {"text":"digit_4", "className":"Button", "index":5, "bounds":"16,667,173,858", "resourceId":"com.android.calculator2:id/digit_4", "type":"clickable", "isParent":true}, {"text":"digit_5", "className":"Button", "index":6, "bounds":"173,667,331,858", "resourceId":"com.android.calculator2:id/digit_5", "type":"clickable", "isParent":true}, {"text":"digit_6", "className":"Button", "index":7, "bounds":"331,667,488,858", "resourceId":"com.android.calculator2:id/digit_6", "type":"clickable", "isParent":true}, {"text":"digit_1", "className":"Button", "index":8, "bounds":"16,858,173,1049", "resourceId":"com.android.calculator2:id/digit_1", "type":"clickable", "isParent":true}, {"text":"digit_2", "className":"Button", "index":9, "bounds":"173,858,331,1049", "resourceId":"com.android.calculator2:id/digit_2", "type":"clickable", "isParent":true}, {"text":"digit_3", "className":"Button", "index":10, "bounds":"331,858,488,1049", "resourceId":"com.android.calculator2:id/digit_3", "type":"clickable", "isParent":true}, {"text":"dec_point", "className":"Button", "index":11, "bounds":"16,1049,173,1240", "resourceId":"com.android.calculator2:id/dec_point", "type":"clickable", "isParent":true}, {"text":"digit_0", "className":"Button", "index":12, "bounds":"173,1049,331,1240", "resourceId":"com.android.calculator2:id/digit_0", "type":"clickable", "isParent":true}, {"text":"eq", "className":"Button", "index":13, "bounds":"331,1049,488,1240", "resourceId":"com.android.calculator2:id/eq", "type":"clickable", "isParent":true}, {"text":"DEL del", "className":"Button", "index":14, "bounds":"512,468,664,621", "resourceId":"com.android.calculator2:id/del", "type":"clickable", "isParent":true}, {"text":"op_div", "className":"Button", "index":15, "bounds":"512,621,664,774", "resourceId":"com.android.calculator2:id/op_div", "type":"clickable", "isParent":true}, {"text":"op_mul", "className":"Button", "index":16, "bounds":"512,774,664,927", "resourceId":"com.android.calculator2:id/op_mul", "type":"clickable", "isParent":true}, {"text":"op_sub", "className":"Button", "index":17, "bounds":"512,927,664,1080", "resourceId":"com.android.calculator2:id/op_sub", "type":"clickable", "isParent":true}, {"text":"op_add", "className":"Button", "index":18, "bounds":"512,1080,664,1232", "resourceId":"com.android.calculator2:id/op_add", "type":"clickable", "isParent":true}, {"text":"Advanced Operations pad_advanced", "className":"ViewGroup", "index":19, "bounds":"672,452,720,1280", "resourceId":"com.android.calculator2:id/pad_advanced", "type":"clickable", "isParent":true}, {"text":"No formula formula", "className":"TextView", "index":20, "bounds":"0,160,720,299", "resourceId":"com.android.calculator2:id/formula", "type":"text", "isParent":false, "parentIndex":1}, {"text":"INV toggle_inv", "className":"Button", "index":21, "bounds":"708,476,720,628", "resourceId":"com.android.calculator2:id/toggle_inv", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"sin fun_sin", "className":"Button", "index":22, "bounds":"708,628,720,781", "resourceId":"com.android.calculator2:id/fun_sin", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"ln fun_ln", "className":"Button", "index":23, "bounds":"708,781,720,934", "resourceId":"com.android.calculator2:id/fun_ln", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"const_pi", "className":"Button", "index":24, "bounds":"708,934,720,1087", "resourceId":"com.android.calculator2:id/const_pi", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"lparen", "className":"Button", "index":25, "bounds":"708,1087,720,1240", "resourceId":"com.android.calculator2:id/lparen", "type":"clickable", "isParent":false, "parentIndex":19}], "screenshot_url":"https://boe.example.invalid/resources/RESOURCE_EXAMPLE_01.png"}}',
+ detail: {
+ tool: 'phone_start_app',
+ run_id: '3a0e5e9a-bf55-4bb1-bf8d-bd7a19a9dd0d',
+ output: {
+ content:
+ '{"result":"Started com.android.calculator2", "current_state":{"clickable_elements":[{"text":"More Options", "className":"ImageButton", "index":0, "bounds":"640,56,720,152", "resourceId":"", "type":"clickable", "isParent":true}, {"text":"digit_7", "className":"Button", "index":2, "bounds":"16,476,173,667", "resourceId":"com.android.calculator2:id/digit_7", "type":"clickable", "isParent":true}, {"text":"digit_8", "className":"Button", "index":3, "bounds":"173,476,331,667", "resourceId":"com.android.calculator2:id/digit_8", "type":"clickable", "isParent":true}, {"text":"digit_9", "className":"Button", "index":4, "bounds":"331,476,488,667", "resourceId":"com.android.calculator2:id/digit_9", "type":"clickable", "isParent":true}, {"text":"digit_4", "className":"Button", "index":5, "bounds":"16,667,173,858", "resourceId":"com.android.calculator2:id/digit_4", "type":"clickable", "isParent":true}, {"text":"digit_5", "className":"Button", "index":6, "bounds":"173,667,331,858", "resourceId":"com.android.calculator2:id/digit_5", "type":"clickable", "isParent":true}, {"text":"digit_6", "className":"Button", "index":7, "bounds":"331,667,488,858", "resourceId":"com.android.calculator2:id/digit_6", "type":"clickable", "isParent":true}, {"text":"digit_1", "className":"Button", "index":8, "bounds":"16,858,173,1049", "resourceId":"com.android.calculator2:id/digit_1", "type":"clickable", "isParent":true}, {"text":"digit_2", "className":"Button", "index":9, "bounds":"173,858,331,1049", "resourceId":"com.android.calculator2:id/digit_2", "type":"clickable", "isParent":true}, {"text":"digit_3", "className":"Button", "index":10, "bounds":"331,858,488,1049", "resourceId":"com.android.calculator2:id/digit_3", "type":"clickable", "isParent":true}, {"text":"dec_point", "className":"Button", "index":11, "bounds":"16,1049,173,1240", "resourceId":"com.android.calculator2:id/dec_point", "type":"clickable", "isParent":true}, {"text":"digit_0", "className":"Button", "index":12, "bounds":"173,1049,331,1240", "resourceId":"com.android.calculator2:id/digit_0", "type":"clickable", "isParent":true}, {"text":"eq", "className":"Button", "index":13, "bounds":"331,1049,488,1240", "resourceId":"com.android.calculator2:id/eq", "type":"clickable", "isParent":true}, {"text":"DEL del", "className":"Button", "index":14, "bounds":"512,468,664,621", "resourceId":"com.android.calculator2:id/del", "type":"clickable", "isParent":true}, {"text":"op_div", "className":"Button", "index":15, "bounds":"512,621,664,774", "resourceId":"com.android.calculator2:id/op_div", "type":"clickable", "isParent":true}, {"text":"op_mul", "className":"Button", "index":16, "bounds":"512,774,664,927", "resourceId":"com.android.calculator2:id/op_mul", "type":"clickable", "isParent":true}, {"text":"op_sub", "className":"Button", "index":17, "bounds":"512,927,664,1080", "resourceId":"com.android.calculator2:id/op_sub", "type":"clickable", "isParent":true}, {"text":"op_add", "className":"Button", "index":18, "bounds":"512,1080,664,1232", "resourceId":"com.android.calculator2:id/op_add", "type":"clickable", "isParent":true}, {"text":"Advanced Operations pad_advanced", "className":"ViewGroup", "index":19, "bounds":"672,452,720,1280", "resourceId":"com.android.calculator2:id/pad_advanced", "type":"clickable", "isParent":true}, {"text":"No formula formula", "className":"TextView", "index":20, "bounds":"0,160,720,299", "resourceId":"com.android.calculator2:id/formula", "type":"text", "isParent":false, "parentIndex":1}, {"text":"INV toggle_inv", "className":"Button", "index":21, "bounds":"708,476,720,628", "resourceId":"com.android.calculator2:id/toggle_inv", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"sin fun_sin", "className":"Button", "index":22, "bounds":"708,628,720,781", "resourceId":"com.android.calculator2:id/fun_sin", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"ln fun_ln", "className":"Button", "index":23, "bounds":"708,781,720,934", "resourceId":"com.android.calculator2:id/fun_ln", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"const_pi", "className":"Button", "index":24, "bounds":"708,934,720,1087", "resourceId":"com.android.calculator2:id/const_pi", "type":"clickable", "isParent":false, "parentIndex":19}, {"text":"lparen", "className":"Button", "index":25, "bounds":"708,1087,720,1240", "resourceId":"com.android.calculator2:id/lparen", "type":"clickable", "isParent":false, "parentIndex":19}], "screenshot_url":"https://boe.example.invalid/resources/RESOURCE_EXAMPLE_01.png"}}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'phone_start_app',
+ id: null,
+ tool_call_id: 'call_PBFe8QY5TUgeMNfwWuoPHTXW',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1752193304586,
+ },
+];
+
+export const mockPhoneChunk = {
+ id: '1753758497627_83863269',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content: 'If you need a code or if you have a problem, let me know.',
+ detail: {
+ attachments: [],
+ scene: 'phone',
+ intent_type: 'asking_user',
+ },
+ timestamp: 1753758497635,
+};
+
+export const mockInnerMessageChunk: InnerMessageChunk = {
+ id: 'MESSAGE_ID_EXAMPLE',
+ role: 'inner_message',
+ type: 'config',
+ content: 'update_session',
+ detail: {
+ session_id: 'SESSION_ID_EXAMPLE',
+ sandbox_id: 'SANDBOX_ID_EXAMPLE',
+ instance_no: 'INSTANCE_ID_EXAMPLE',
+ access_key: 'ACCESS_KEY_EXAMPLE',
+ access_secret_key: 'SECRET_KEY_EXAMPLE',
+ expire_time: '2099-12-31 23:59:59',
+ user_id: 'USER_ID_EXAMPLE',
+ },
+ step_id: null,
+ timestamp: 1754466924252,
+ is_llm_message: false,
+ session_id: 'dcc8b24ea48445b0',
+ task_id: '1754466888313_wk74',
+};
+
+export const mockPPTChunk = {
+ id: 138054,
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ '🎉 Marx themePPTProduction complete!\n\nI\'ve made you a beautiful Marx.PPTPresentation, all5Pages\n\n📋 **PPTOverview of content:**\n1. **Cover Page** - Marxist great ideas.\n2. **Contents Page** - Including the life of Max, the main works, the core ideas, the historical implications, the modern meaning of the term\n3. **Max is a living man.** - Chapter Separator Pages\n4. **Life details** - It includes tables on the major life stages and charts on the development of ideas.\n5. **Ideological influence** - Impact analysis of the three main components of Marxism\n\n📁 **File format:**\n- PowerPointDocumentation(.pptx) - Editable Presentation\n- PDFDocumentation(.pdf) - Easy to share and view\n\n✨ The file is ready. Please download it!',
+ detail: {
+ attachments: [
+ {
+ filename: 'Max.-Great thinker..pptx',
+ path: '/workspace/Max.-Great thinker..pptx',
+ url: 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_02.pptx',
+ size: 3600066,
+ content_type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ show_user: 1,
+ },
+ {
+ filename: 'Max.-Great thinker..pdf',
+ path: '/workspace/Max.-Great thinker..pdf',
+ url: 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_03.pdf',
+ size: 1033993,
+ content_type: 'application/pdf',
+ show_user: 1,
+ },
+ {
+ key: 'c105272cc835333a8abf927e52e35a3f',
+ url: 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_04',
+ name: 'testTemplates.pptx',
+ size: 6045501,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ intent_type: 'task_completed',
+ scene: 'not_phone',
+ },
+ step_id: null,
+ timestamp: 1754991960,
+ is_llm_message: false,
+ session_id: '135db4d8a8834850',
+};
+
+export const pptChunks = [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://storage.example.invalid/templates/TEMPLATE_EXAMPLE.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ },
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ },
+ {
+ id: '1756784071572_753e6987',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'choose_and_read_template_pptx_info',
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ result: {
+ content:
+ '{"content": "[\\n {\\n \\"slide_number\\": 1,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"be179f3b\\": [\\n \\"01AI Presentation\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 2,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"83b517ce\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"682a9d2a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"64b215f0\\": [\\n \\"04\\"\\n ],\\n \\"19a37359\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"1aa366ef\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"db61c16c\\": [\\n \\"02\\"\\n ],\\n \\"9dd5986c\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"eb5d339f\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"8b642dcc\\": [\\n \\"03\\"\\n ],\\n \\"9e319fcb\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"ee8da08a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"f2a48842\\": [\\n \\"01\\"\\n ],\\n \\"8351ea2e\\": [\\n \\"Contents\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 3,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"a9775c2a\\": [\\n \\"The introduction to the 101st.\\"\\n ],\\n \\"722f74e7\\": [\\n \\"“01.AI.”• Digitalization, based on zero and one, to connect and manage everything with intelligence, and to promote digital transformation and intellectual social building.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 4,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"f9ee6954\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platform, which will effectively help to raise human productivity and thus create huge economic and social values with a strong technological vision and artificial intelligence belief, and zero things dedicated to building a whole new one AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"c15b6480\\": [\\n \\"Text Titles\\"\\n ],\\n \\"a34fe09f\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"dcfced38\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"5655a992\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"9f58fe6c\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"4a064350\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"23129021\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"96cc470d\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"309ca158\\": [\\n \\"AI 2.0 Platform\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 5,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"eae66713\\": [\\n \\"The Philosophy and Maths Meaning of 01\\"\\n ],\\n \\"14b01155\\": [\\n \\"Zero Origin and Development\\"\\n ],\\n \\"4b8d5330\\": [\\n \\"The symbolism of the first.\\"\\n ],\\n \\"e1543e6b\\": [\\n \\"Zero and one in mathematics.\\"\\n ],\\n \\"a1b45b87\\": [\\n \\"The concept of zero is rooted in ancient civilization, where Indian mathematicians use zero as an independent digital invention to promote the improvement of the mathematical system and scientific progress.\\"\\n ],\\n \\"7d8cb902\\": [\\n \\"The first symbol of unity and starting point of a number is the beginning and multiplication of the natural number, representing the unity of the whole and the source of all things.\\"\\n ],\\n \\"75ef77d4\\": [\\n \\"The zero and one are mathematical building blocks, zero are plus and minus elements of identity, and one is the multiplication core, which forms the basis of the digital system and facilitates computing and information processing.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 6,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"fc354462\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"81c521dc\\": [\\n \\"Binary system base\\"\\n ],\\n \\"c9ff1628\\": [\\n \\"How computers operate with zero and one.\\"\\n ],\\n \\"477c00b4\\": [\\n \\"Zero Encoding and Data Storage\\"\\n ],\\n \\"903bf9c9\\": [\\n \\"1\\"\\n ],\\n \\"9c00c483\\": [\\n \\"The binary system, which expresses all data and instructions in zero and one or two states, is the core language of computers and digital equipment, simplifys circuit design and ensures accurate data transmission.\\"\\n ],\\n \\"131611a9\\": [\\n \\"2\\"\\n ],\\n \\"7fe0b43b\\": [\\n \\"Computers convert information to binary codes, perform arithmetic and logical operations through the logical door, and achieve complex data processing and program execution.\\"\\n ],\\n \\"52f74783\\": [\\n \\"3\\"\\n ],\\n \\"ff45f2a8\\": [\\n \\"The 01 code converts information to binary format suitable for storage and transmission, and common code is availableASCIIandUnicode,Storage devices preserve data through charge change.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 7,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"c5d8ade1\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"b0993f80\\": [\\n \\"The way things are digitally realized.\\"\\n ],\\n \\"5e3209a4\\": [\\n \\"The seamless connection of entities to the digital world is achieved through sensor collection of data, real-time network transmission, cloud computing storage analysis and smart algorithm decision-making.\\"\\n ],\\n \\"27103ac8\\": [\\n \\"101 applications in the Internet\\"\\n ],\\n \\"71712164\\": [\\n \\"The 01 Code serves as the basis for binary development, ensuring the accurate transmission of data and efficient collaboration of equipment in the Internet of goods, and promoting intelligent home and intelligent urban development.\\"\\n ],\\n \\"de614814\\": [\\n \\"Digital twinning technology profile\\"\\n ],\\n \\"31b55279\\": [\\n \\"The integration of the digital economy is facilitated by the use of zero data combined with three-dimensional modelling to optimize real-time mapping, failure prediction and maintenance of physical equipment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 8,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"dff43bdd\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"75d19ee0\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"c4a426df\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"00e037d3\\": [\\n \\"Zero One Technology for Industrial Change\\"\\n ],\\n \\"b4de8dbb\\": [\\n \\"The zero-one technology promotes manufacturing intelligence, precision management in agriculture and digitalization of services, and the re-establishment of industrial chains and high-quality economic development.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 9,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"4b7576d4\\": [\\n \\"The challenges and problems of 01.AI.\\"\\n ],\\n \\"a1ca8c3f\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"f9176f86\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"fbec61e5\\": [\\n \\"Technological divide and digital exclusion\\"\\n ],\\n \\"e77ae3d9\\": [\\n \\"The technological divide, which leads to the digital exclusion of some groups, requires the reduction of the urban-rural gap, the upgrading of digital literacy and the expansion of infrastructure to achieve inclusive development in a digital society.\\"\\n ],\\n \\"b9b4bb28\\": [\\n \\"Ethical and legal issues\\"\\n ],\\n \\"67598b22\\": [\\n \\"Development of zero-sum objects presents ethical and legal challenges, requiring the establishment of regulatory and legal frameworks that guarantee the legitimate use of data, the transparency of artificial intelligence and a rational response to the effects of employment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 10,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"5144e569\\": [\\n \\"The future of 01.AI.\\"\\n ],\\n \\"db5ff530\\": [\\n \\"Integration of artificial intelligence with the one technology\\"\\n ],\\n \\"2e7e6eec\\": [\\n \\"The zero one technology provides the basic data expression and computing of artificial intelligence and promotes the upgrading of the intelligence system through the chain.\\"\\n ],\\n \\"7f245244\\": [\\n \\"Impact of quantum calculations on the 101 system\\"\\n ],\\n \\"bdbde1cb\\": [\\n \\"Quantum computing has broken the traditional 01 limit, promoting the development of classic Zero-one technologies and quantum computing in combination, and expanding the boundaries of computing capabilities.\\"\\n ],\\n \\"0a629dfc\\": [\\n \\"Smart cities and the construction of intelligent societies\\"\\n ],\\n \\"e2197e6e\\": [\\n \\"Zero-one technology drives the smart city core system to achieve intelligent management and improve quality of life and the efficiency of urban operations.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 11,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"61d550f6\\": [\\n \\"Summary and reflection\\"\\n ],\\n \\"86dba575\\": [\\n \\"A review of the core values of 01.AI.\\"\\n ],\\n \\"f4abbd51\\": [\\n \\"The zero-sum potential of the basic unit of information is reflected in the underlying concepts of the digital age, driving the digital transformation and industrial upgrading.\\"\\n ],\\n \\"4428e62e\\": [\\n \\"Digital thinking for the future\\"\\n ],\\n \\"cd06c39d\\": [\\n \\"The integration of emerging technologies has driven the expansion of the application of 101 things, and digital transformations have transformed productive life, with attention to equity and data security.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 12,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"33017331\\": [\\n \\"Thank you.\\"\\n ]\\n }\\n }\\n }\\n]", "content_type": "markdown", "status": "success", "template_path": "/tmp/doc/uploaded_template_109f091d397042ed_01.AI.-Light.pptx"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'choose_and_read_template_pptx_info',
+ id: '47c60773-a8ec-42a4-966f-7fc775888fcd',
+ tool_call_id: 'tooluse_Zk6nBNkWQSSo41jwhYZKPQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784071578,
+ },
+ {
+ id: '1756784075645_f22d85ed',
+ role: 'assistant',
+ type: 'plan_update',
+ content: 'Note on planned advancement',
+ detail: {
+ action: 'update',
+ steps: [
+ {
+ id: '1',
+ status: 'success',
+ started_at: 1756784075645,
+ },
+ {
+ id: '2',
+ status: 'running',
+ started_at: 1756784075645,
+ },
+ ],
+ },
+ timestamp: 1756784075650,
+ },
+ {
+ id: '1756784081680_a1b2e38f',
+ role: 'assistant',
+ type: 'tool_call',
+ content: 'Search for basic information, founders, products and development of the company.',
+ detail: {
+ run_id: 'e38d7f44-400b-4a86-9708-b9c1d2386dec',
+ tool: 'web_search',
+ status: 'pending',
+ param: {
+ query: 'The company of 01.AI and 10th. 01.AI Lee\'s A.I., Big Model. YiSeries',
+ brief: 'Search for basic information, founders, products and development of the company.',
+ },
+ action: 'Searching',
+ action_content: 'The company of 01.AI and 10th. 01.AI Lee\'s A.I., Big Model. YiSeries',
+ },
+ timestamp: 1756784084497,
+ },
+ {
+ id: '1756784084528_93acb728',
+ role: 'assistant',
+ type: 'tool_result',
+ content: 'Search for basic information, founders, products and development of the company.',
+ detail: {
+ tool: 'web_search',
+ run_id: 'e38d7f44-400b-4a86-9708-b9c1d2386dec',
+ result: {
+ content:
+ '[{"title": "Yi Foundation Models - 01.AI.-AI2.0Global companies for large model technologies and applications ...", "text": "", "metadata": {"snippet": "Missing: Lee\'s recovery, artificial intelligence, series.", "score": 0.9219217896461487, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/yi-models"}, "num": 1, "connector_name": "search_one_v3"}, {"title": "The new company Li Chong-Chang is named 01.AI, and the new company is called "Li Chong-Chang".“BuildAI2.0New platform. Speed it up.AGICome on.”", "text": "", "metadata": {"snippet": "Missing: Yi Series", "score": 0.9136765599250793, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://www.chuangxin.com/blog/ai2-0-agi"}, "num": 2, "connector_name": "search_one_v3"}, {"title": "Lee Kai-chul.4More than a month later,“Zoom in.”:AlignOpenAI、Google, release.“The world\'s strongest.”Open Source ...", "text": "", "metadata": {"snippet": "Today, it was built by Li Kai-chul.AI Big Model Startup“01.AI.”A series of large open source models were published:Yi-34B andYi-6B。 Yi-34B It\'s a bilingual (English and Chinese) basic model, passing through.340 Billions of arguments ...", "score": 0.851952850818634, "engine": "google", "date_published": "Nov 6, 2023", "favicon": "", "is_full_text": false, "url": "https://www.infoq.cn/article/3m7f87qpdvsu8zv68k1b"}, "num": 3, "connector_name": "search_one_v3"}, {"title": "Chinese1.0 - 01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "Missing: Lee Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-Cai-C Li, Li, Li, Li-S-Sing-S", "score": 0.8499711751937866, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/cn1"}, "num": 4, "connector_name": "search_one_v3"}, {"title": "Chinese- 01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "The same size open source model is strongest · Yi Series Model Enhancement · Full improvement in code, mathematics, reasoning and command compliance · Multiple context lengths (4K, 16K, 32K) · Free commercial application.", "score": 0.8006919622421265, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/cn"}, "num": 5, "connector_name": "search_one_v3"}, {"title": "Commercial applications- 01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "01.AI encourages authorized use.Yi The following principles are followed in the development and deployment of artificial intelligence products in the series:. 4.1.1 The authorized person ensures his/her right.Yidevelopment, use, and based onYiSeries Model ...", "score": 0.7185943722724915, "engine": "google", "date_published": "Nov 23, 2023", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/yi-license"}, "num": 6, "connector_name": "search_one_v3"}, {"title": "01-ai/Yi: A series of large language models trained from ... - GitHub", "text": "", "metadata": {"snippet": "The opening of 01.AI.Yi-VLA big model of polymodes, a magic community reasoning.&Here comes the best practice! 2024-01-26, ModelScope ; Single card 3 Hourly training Yi-6B Large model Agent:Based on Llama Factory The field., 2024-01-22 ...", "score": 0.6909762620925903, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://github.com/01-ai/Yi"}, "num": 7, "connector_name": "search_one_v3"}, {"title": "Bloomberg Dialogue Lee Zhui: What I want to do is fill in.AIThe circle doesn\'t exist yet.“Windows”It\'s... ...", "text": "", "metadata": {"snippet": "So, all of 01.AI things are in a full hug.DeepSeek,Launch of the Big Elite Enterprise One-Stop platform, which not only provides security model deployment programmes, model fine-tuning programmes, but also covers online search, knowledge baseRAG、AgentBuilding up. ...", "score": 0.6495786309242249, "engine": "google", "date_published": "Mar 20, 2025", "favicon": "", "is_full_text": false, "url": "https://www.lingyiwanwu.com/blog/ai-windows"}, "num": 8, "connector_name": "search_one_v3"}, {"title": "01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "Missing: Artificial Intelligence Series", "score": 0.5936092734336853, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/"}, "num": 9, "connector_name": "search_one_v3"}, {"title": "01.AI.-AI2.0Global companies for large model technologies and applications", "text": "", "metadata": {"snippet": "01.AI is dedicated to becoming an innovative enterprise driven by a technological vision and with a strong Chinese engineering base, and to promoting a ground-based model.AI 2.0The government has been able to create a revolution in technology and platform to apply it at multiple levels.", "score": 0.47852879762649536, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://www.lingyiwanwu.com/"}, "num": 10, "connector_name": "search_one_v3"}]',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'web_search',
+ id: '373e8a8c-fb15-4d23-bad4-5845c891af84',
+ tool_call_id: 'tooluse_kwu0hkfeSKK0bjdQ0mXRVQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784084537,
+ },
+ {
+ id: '1756784088906_0af1ab01',
+ role: 'assistant',
+ type: 'tool_call',
+ content: 'Search for the funding of 01.AI, the size of the team, the technical features and business models',
+ detail: {
+ run_id: '3c039e36-edd1-491e-a2fd-f400321bd5c1',
+ tool: 'web_search',
+ status: 'pending',
+ param: {
+ brief: 'Search for the funding of 01.AI, the size of the team, the technical features and business models',
+ query: '01.AI. 01.AI Financial valuation team technical features business model AI2.0',
+ },
+ action: 'Searching',
+ action_content: '01.AI. 01.AI Financial valuation team technical features business model AI2.0',
+ },
+ timestamp: 1756784090927,
+ },
+ {
+ id: '1756784090955_0934d70b',
+ role: 'assistant',
+ type: 'tool_result',
+ content: 'Search for the funding of 01.AI, the size of the team, the technical features and business models',
+ detail: {
+ tool: 'web_search',
+ run_id: '3c039e36-edd1-491e-a2fd-f400321bd5c1',
+ result: {
+ content:
+ '[{"title": "01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "Missing: Finance estimates Team characteristics Business model", "score": 0.9399133324623108, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/"}, "num": 1, "connector_name": "search_one_v3"}, {"title": "Chinese- 01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "Missing: Financing Team Characteristics Business Model", "score": 0.854884684085846, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/cn"}, "num": 2, "connector_name": "search_one_v3"}, {"title": "Chinese1.0 - 01.AI.-AI2.0Global companies for large model technologies and applications (GMOs)01.AI)", "text": "", "metadata": {"snippet": "Missing: Financing Team Characteristics", "score": 0.7718434929847717, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "http://www.01.ai/cn1"}, "num": 3, "connector_name": "search_one_v3"}, {"title": "01.AI.-AI2.0Global companies for large model technologies and applications", "text": "", "metadata": {"snippet": "Missing: Financial characteristics", "score": 0.6169357895851135, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://www.lingyiwanwu.com/"}, "num": 4, "connector_name": "search_one_v3"}, {"title": "Lee is on the line with the big model company, and dozens of core members are in position. - iBlack Horse.", "text": "", "metadata": {"snippet": "This year3Late in the month, the chairman and chairman of the innovation workshop.CEOLee is returning to the official announcementProject AI2.0It\'s a big model of entry.……And 01.AI is the official name of the company that started it.The official 01.AI website also launched that day.", "score": 0.5324251055717468, "engine": "google", "date_published": "Jul 3, 2023", "favicon": "", "is_full_text": false, "url": "https://www.iheima.com/article-358877.html"}, "num": 5, "connector_name": "search_one_v3"}, {"title": "01.AI.- Innovative workshops", "text": "", "metadata": {"snippet": "AI 2.0 The platform will be ten times as powerful as mobile Internet, and this is the first time China has been competing. All user portals and interfaces on the new platform will be rewritten, and companies that can build a next generation will have a huge advantage. ...", "score": 0.5228112936019897, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://www.chuangxin.com/ai2"}, "num": 6, "connector_name": "search_one_v3"}, {"title": "Lee\'s re-enactment.AICompany 01.AI. - funnpen Turn it over.", "text": "", "metadata": {"snippet": "Missing: Estimated value of financing", "score": 0.5070796012878418, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://www.funnpen.com/index.php?action=article_detail&id=996"}, "num": 7, "connector_name": "search_one_v3"}, {"title": "Lee\'s rediscovering it.AIThe company\'s 1001.AI dollars is overvalued.10US$ billion - Electrician pie", "text": "", "metadata": {"snippet": "Missing: Speciality Commercial", "score": 0.4668455719947815, "engine": "google", "date_published": "", "favicon": "", "is_full_text": false, "url": "https://www.pai.com.cn/231727.html"}, "num": 8, "connector_name": "search_one_v3"}, {"title": "And I\'ll get the 10 or 10th.ARound finance, accelerationAILarge model development - - Billion euros.", "text": "", "metadata": {"snippet": "Beijing 10th Zone Technology Ltd. (abbreviated: 101th Zone)ARounds of financing, investors have not yet disclosed.AILarge model development, boostingAI 2.0Technological revolution.", "score": 0.45519864559173584, "engine": "google", "date_published": "Aug 7, 2024", "favicon": "", "is_full_text": false, "url": "https://www.iyiou.com/data/202408071074196"}, "num": 9, "connector_name": "search_one_v3"}, {"title": "Waves.AIGCFirst time I\'ve seen Lee\'s new company: name.“01.AI.”,The self-study model will be developed.", "text": "", "metadata": {"snippet": "On the technical path, the zero-sum selected the self-study model path. It is known that the zero-zero-nine split the development of large-scale modelling platforms into seven-module technology road maps, with a pre-training framework (see figure II).Pre-train)Technology, Post-trainTechnology, ...", "score": 0.3302551507949829, "engine": "google", "date_published": "Jul 3, 2023", "favicon": "", "is_full_text": false, "url": "https://www.21jingji.com/article/20230703/herald/8bfff073a13223296e16278fb3137e64.html"}, "num": 10, "connector_name": "search_one_v3"}]',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'web_search',
+ id: '70aff0a9-aaa4-4ee6-b6e9-f109f021a42b',
+ tool_call_id: 'tooluse_Cf02bRMdQ6SO1gLZHoopGQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784090961,
+ },
+ {
+ id: '1756784096122_8a7dfaa3',
+ role: 'assistant',
+ type: 'plan_update',
+ content: 'Note on planned advancement',
+ detail: {
+ action: 'update',
+ steps: [
+ {
+ id: '2',
+ status: 'success',
+ started_at: 1756784096122,
+ },
+ {
+ id: '3',
+ status: 'running',
+ started_at: 1756784096122,
+ },
+ ],
+ },
+ timestamp: 1756784096126,
+ },
+ {
+ id: '1756784109120_f91a2954',
+ role: 'assistant',
+ type: 'plan_update',
+ content: 'Note on planned advancement',
+ detail: {
+ action: 'update',
+ steps: [
+ {
+ id: '3',
+ status: 'success',
+ started_at: 1756784109120,
+ },
+ {
+ id: '4',
+ status: 'running',
+ started_at: 1756784109120,
+ },
+ ],
+ },
+ timestamp: 1756784109130,
+ },
+ {
+ id: '1756784115416_4ff9c5a7',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '1f8b7d68-e586-4750-9286-f54f3de5cd02',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 1,
+ page_index: 1,
+ replace_content: '{"text": {"be179f3b": ["The company introduced 01.AI."]}}',
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784118660,
+ },
+ {
+ id: '1756784118672_ef4be35d',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '1f8b7d68-e586-4750-9286-f54f3de5cd02',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 1 Page production completed**\\n📄 **Page Title**: The company introduced 01.AI.\\n📝 **Page Content Outline**:\\n 1. The company introduced 01.AI.\\n📊 **Statistics**: 1 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 1, "template_page_index": 1, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: 'e3ebdfad-90b4-4aa4-9936-b3ff88f9375c',
+ tool_call_id: 'tooluse_qb89cluwTLemNDDVnU0YEQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784118693,
+ },
+ {
+ id: '1756784127078_6c260e2f',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '0b57856b-f0c1-42fb-853b-5c1b88b879ac',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 2,
+ replace_content:
+ '{"text": {"8351ea2e": ["Contents"], "f2a48842": ["01"], "ee8da08a": ["Corporate profile"], "9e319fcb": ["Zero-one-thousand-all basic information, founding team, development history."], "8b642dcc": ["02"], "eb5d339f": ["Core technology"], "9dd5986c": ["AI 2.0Platform,YiTechnical features of the series of large models"], "db61c16c": ["03"], "1aa366ef": ["Products and applications"], "19a37359": ["YiSeries models, commercial applications"], "64b215f0": ["04"], "682a9d2a": ["Future outlook"], "83b517ce": ["Development planning, technology trends, market prospects"]}}',
+ page_index: 2,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784129607,
+ },
+ {
+ id: '1756784129648_aca0d724',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '0b57856b-f0c1-42fb-853b-5c1b88b879ac',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 2 Page production completed**\\n📄 **Page Title**: Contents\\n📝 **Page Content Outline**:\\n 1. Contents\\n 2. 01\\n 3. Corporate profile\\n 4. Zero-one-thousand-all basic information, founding team, development history.\\n 5. 02\\n 6. Core technology\\n 7. AI 2.0Platform,YiTechnical features of the series of large models\\n 8. 03\\n 9. Products and applications\\n 10. YiSeries models, commercial applications\\n 11. 04\\n 12. Future outlook\\n 13. Development planning, technology trends, market prospects\\n📊 **Statistics**: 13 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 2, "template_page_index": 2, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: 'c26a3f13-2880-451b-b000-1a0b965c3816',
+ tool_call_id: 'tooluse_mxC1r2aCQyKYOs9lJSQNJw',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784129661,
+ },
+ {
+ id: '1756784135737_8b8588b0',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '86e63264-0c50-4bc2-be55-246f9eecc0f2',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 3,
+ replace_content:
+ '{"text": {"a9775c2a": ["The company profile for the 101st."], "722f74e7": ["- What?01.AI)It was created by Li Kai-chul.AI 2.0Large Model Technology, dedicated to building a new artificial intelligence platform. The company focuses on base model development with a strong technological vision and artificial intelligence belief.AITechnological revolutionary breakthroughs at multiple levels have helped to increase human productivity and create great economic and social values."]}}',
+ page_index: 3,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784138987,
+ },
+ {
+ id: '1756784139006_0104834b',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '86e63264-0c50-4bc2-be55-246f9eecc0f2',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 3 Page production completed**\\n📄 **Page Title**: The company profile for the 101st.\\n📝 **Page Content Outline**:\\n 1. The company profile for the 101st.\\n 2. - What?01.AI)It was created by Li Kai-chul.AI 2.0Large Model Technology, dedicated to building a new artificial intelligence platform. The company focuses on base model development with a strong technological vision and artificial intelligence belief.AIThe technology has revolutionized at multiple levels, helping to boost it....\\n📊 **Statistics**: 2 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 3, "template_page_index": 3, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '0a5b1020-1aad-45c7-b430-bef60ebc82fc',
+ tool_call_id: 'tooluse_GVbjwBxjQBKNV7JddV1bGA',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784139029,
+ },
+ {
+ id: '1756784151655_ab6958d3',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: 'b52181b4-b198-49d7-8870-ee7de14af714',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 4,
+ replace_content:
+ '{"text": {"c15b6480": ["AI 2.0Platform vision"], "f9ee6954": ["And all of it is built.AI 2.0The new platform, a platform ten times larger than mobile Internet, is a platform. Through the base-based large model technology, all user accesses and interfaces are rewritten to provide smart solutions for businesses and individuals to drive the digital transformation of industry."], "dcfced38": ["Technological innovation"], "a34fe09f": ["Self-studyYiA large series of models covering multiple parameters sizes, with excellent performance in code, mathematics, reasoning and command compliance, supporting multiple context lengths."], "9f58fe6c": ["Open source ecology"], "5655a992": ["ReleaseYi-34BandYi-6BWait for open source models, pushAITechnology diffusion, open-endedAIEcosystems, promoting synergistic industrial development."], "23129021": ["Commercial applications"], "4a064350": ["Provides a one-stop platform for a large-scale model for a think-tank enterprise, including security model deployment, fine-tuning programmes, networking search, knowledge baseRAG、AgentBuild, etc."], "309ca158": ["Future Layout"], "96cc470d": ["FaceAGIThe age, continuing to invest in basic research, exploring frontier technologies such as polymodes, context, etc., to build the next generationAIInfrastructure."]}}',
+ page_index: 4,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784154802,
+ },
+ {
+ id: '1756784154832_a2165fd3',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: 'b52181b4-b198-49d7-8870-ee7de14af714',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 4 Page production completed**\\n📄 **Page Title**: AI 2.0Platform vision\\n📝 **Page Content Outline**:\\n 1. AI 2.0Platform vision\\n 2. And all of it is built.AI 2.0The new platform, a platform ten times larger than mobile Internet, is a platform. Through the base-based large model technology, all user accesses and interfaces are rewritten to provide smart solutions for businesses and individuals to drive the digital transformation of industry.\\n 3. Technological innovation\\n 4. Self-studyYiA large series of models covering multiple parameters sizes, with excellent performance in code, mathematics, reasoning and command compliance, supporting multiple context lengths.\\n 5. Open source ecology\\n 6. ReleaseYi-34BandYi-6BWait for open source models, pushAITechnology diffusion, open-endedAIEcosystems, promoting synergistic industrial development.\\n 7. Commercial applications\\n 8. Provides a one-stop platform for a large-scale model for a think-tank enterprise, including security model deployment, fine-tuning programmes, networking search, knowledge baseRAG、AgentBuild, etc.\\n 9. Future Layout\\n 10. FaceAGIThe age, continuing to invest in basic research, exploring frontier technologies such as polymodes, context, etc., to build the next generationAIInfrastructure.\\n📊 **Statistics**: 10 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 4, "template_page_index": 4, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: 'b984d306-a839-407d-bc36-5beab3677b97',
+ tool_call_id: 'tooluse_-Uw5SjrpQg2efB48tocWBQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784154840,
+ },
+ {
+ id: '1756784165184_7920f5bf',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '6da71e37-4e90-4d99-bd7a-1b7e10913d43',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 5,
+ replace_content:
+ '{"text": {"eae66713": ["One technology philosophy."], "14b01155": ["Zero innovation."], "4b8d5330": ["One unified concept."], "e1543e6b": ["Zero-one.AIof the Convention on the Rights of the Child"], "a1b45b87": ["Zero represents the starting point of infinite possibilities and is the foundation of a digital world.AIArea zero symbolizes the beginning of a vacuum and learning, providing the basis for initialization of model training."], "7d8cb902": ["One for unity and unity, symbolAISystem coherence and integrity. In a large model, one reflects the optimization of parameters for uniform architecture design and coordination."], "75ef77d4": ["The combination of zero forms the basis of binary and is fundamental to all numerical calculations.AI 2.0The era of 01 is not just a data expression, but a cornerstone of the logic of thinking in intelligent systems."]}}',
+ page_index: 5,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784168377,
+ },
+ {
+ id: '1756784168399_7403713e',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '6da71e37-4e90-4d99-bd7a-1b7e10913d43',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 5 Page production completed**\\n📄 **Page Title**: One technology philosophy.\\n📝 **Page Content Outline**:\\n 1. One technology philosophy.\\n 2. Zero innovation.\\n 3. One unified concept.\\n 4. Zero-one.AIof the Convention on the Rights of the Child\\n 5. Zero represents the starting point of infinite possibilities and is the foundation of a digital world.AIArea zero symbolizes the beginning of a vacuum and learning, providing the basis for initialization of model training.\\n 6. One for unity and unity, symbolAISystem coherence and integrity. In a large model, one reflects the optimization of parameters for uniform architecture design and coordination.\\n 7. The combination of zero forms the basis of binary and is fundamental to all numerical calculations.AI 2.0The era of 01 is not just a data expression, but a cornerstone of the logic of thinking in intelligent systems.\\n📊 **Statistics**: 7 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 5, "template_page_index": 5, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '8b94a53d-27cc-4b48-91a1-56c86625cb16',
+ tool_call_id: 'tooluse_Nzvx9NxnR5e2mwxvcz5XLA',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784168418,
+ },
+ {
+ id: '1756784177794_ed502c73',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: 'a885278f-3259-4b46-ac19-931c6f245c98',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 6,
+ replace_content:
+ '{"text": {"fc354462": ["YiSeries of large model technical architecture"], "81c521dc": ["Model Parameter Size"], "c9ff1628": ["Multimodular Capability"], "477c00b4": ["Context Process"], "903bf9c9": ["1"], "9c00c483": ["YiSeries contains6B、34BAnd so on, we\'ll take advanced steps.TransformerStructure, performance leading in open source models of the same size, supporting bilingual processing in Central-British."], "131611a9": ["2"], "7fe0b43b": ["Yi-VLMultimodular mega-models integrate visual and linguistic understanding, support complex tasks such as image analysis, document understanding, and outreachAIApply the boundary."], "52f74783": ["3"], "ff45f2a8": ["Support4K、16K、32KThe length of the context, which meets the needs of different applications, is excellent in understanding and generating long text."]}}',
+ page_index: 6,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784180146,
+ },
+ {
+ id: '1756784180177_4eafad53',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: 'a885278f-3259-4b46-ac19-931c6f245c98',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 6 Page production completed**\\n📄 **Page Title**: YiSeries of large model technical architecture\\n📝 **Page Content Outline**:\\n 1. YiSeries of large model technical architecture\\n 2. Model Parameter Size\\n 3. Multimodular Capability\\n 4. Context Process\\n 5. 1\\n 6. YiSeries contains6B、34BAnd so on, we\'ll take advanced steps.TransformerStructure, performance leading in open source models of the same size, supporting bilingual processing in Central-British.\\n 7. 2\\n 8. Yi-VLMultimodular mega-models integrate visual and linguistic understanding, support complex tasks such as image analysis, document understanding, and outreachAIApply the boundary.\\n 9. 3\\n 10. Support4K、16K、32KThe length of the context, which meets the needs of different applications, is excellent in understanding and generating long text.\\n📊 **Statistics**: 10 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 6, "template_page_index": 6, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '7630f2ce-f6bc-43b4-8cc2-05972eec7210',
+ tool_call_id: 'tooluse_dM0O75gHTaaJmvMB-DKJhg',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784180186,
+ },
+ {
+ id: '1756784188280_54ef6cd8',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '04b22cb8-0055-4b28-9d29-f9ed73fe8850',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 7,
+ replace_content:
+ '{"text": {"c5d8ade1": ["The product ecology of 10th."], "b0993f80": ["Enterprise-level solutions"], "5e3209a4": ["One stop for the Big Model Platform for Man-Size EnterprisesAIServices, including security model deployment, customization fine-tuning, knowledge baseRAG、SmartAgentBuilding functions, etc., to help digitalize enterprises."], "27103ac8": ["Open source model ecology"], "71712164": ["YiSeries open source models support free commercial operations, lowerAIApply thresholds to build open ecology. Provide complete development of the tool chain and technical support to promoteAITechnology diffusion."], "de614814": ["Industry applications"], "31b55279": ["Covers a wide range of sectors, including finance, education, health, manufacturing, and provides diversification of smart customer service, content generation, code aids, data analysis, etc.AIApply the solution."]}}',
+ page_index: 7,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784191505,
+ },
+ {
+ id: '1756784191537_70b1d8be',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '04b22cb8-0055-4b28-9d29-f9ed73fe8850',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 7 Page production completed**\\n📄 **Page Title**: The product ecology of 10th.\\n📝 **Page Content Outline**:\\n 1. The product ecology of 10th.\\n 2. Enterprise-level solutions\\n 3. One stop for the Big Model Platform for Man-Size EnterprisesAIServices, including security model deployment, customization fine-tuning, knowledge baseRAG、SmartAgentBuilding functions, etc., to help digitalize enterprises.\\n 4. Open source model ecology\\n 5. YiSeries open source models support free commercial operations, lowerAIApply thresholds to build open ecology. Provide complete development of the tool chain and technical support to promoteAITechnology diffusion.\\n 6. Industry applications\\n 7. Covers a wide range of sectors, including finance, education, health, manufacturing, and provides diversification of smart customer service, content generation, code aids, data analysis, etc.AIApply the solution.\\n📊 **Statistics**: 7 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 7, "template_page_index": 7, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '6102da36-1563-4981-bc4f-2993b4f75231',
+ tool_call_id: 'tooluse_ycSjfVyTS9WnhLmlljHMeQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784191561,
+ },
+ {
+ id: '1756784198303_e6bc4956',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '6accc9f2-8259-4ab2-a5bd-3464aba27c9f',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 8,
+ replace_content:
+ '{"text": {"dff43bdd": ["The commercial value of 10% of the world\'s goods."], "75d19ee0": ["Industrial smart upgrade"], "c4a426df": ["Pass.AI 2.0Technology promotes the intellectual transformation of traditional industries, improves production efficiency and the quality of decision-making, and creates new business models and value growth points."], "00e037d3": ["Ecosystem development"], "b4de8dbb": ["Build OpenAIEcosystems, linking developers, business users and technical partners, creating a synergistic industrial chain of innovation, and releasingAISize-based value of technology."]}}',
+ page_index: 8,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784201737,
+ },
+ {
+ id: '1756784201780_4b2946ab',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '6accc9f2-8259-4ab2-a5bd-3464aba27c9f',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 8 Page production completed**\\n📄 **Page Title**: The commercial value of 10% of the world\'s goods.\\n📝 **Page Content Outline**:\\n 1. The commercial value of 10% of the world\'s goods.\\n 2. Industrial smart upgrade\\n 3. Pass.AI 2.0Technology promotes the intellectual transformation of traditional industries, improves production efficiency and the quality of decision-making, and creates new business models and value growth points.\\n 4. Ecosystem development\\n 5. Build OpenAIEcosystems, linking developers, business users and technical partners, creating a synergistic industrial chain of innovation, and releasingAISize-based value of technology.\\n📊 **Statistics**: 5 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 8, "template_page_index": 8, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '3d3d4059-e4a1-4812-9a74-d4182b39701f',
+ tool_call_id: 'tooluse_09ED7Kq9RI234uEWq61saQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784201787,
+ },
+ {
+ id: '1756784210719_dc89cfaa',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '4e1688e7-ddcc-40ff-b5ed-763b37d1e9bf',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 9,
+ replace_content:
+ '{"text": {"4b7576d4": ["Development challenges and responses"], "a1ca8c3f": ["Technical challenges"], "f9176f86": ["Large model training requires significant computing resources and data support, as well as continuous optimization of algorithm efficiency, reduced training costs, enhanced model performance and extensive capability."], "fbec61e5": ["Market competition"], "e77ae3d9": ["AILarge model markets are highly competitive and require a leading advantage in technological innovation, product differentiation and ecological construction, as well as sustainable barriers to competition."], "b9b4bb28": ["Compliance and security"], "67598b22": ["AITechnology development faces challenges such as data privacy, algorithm fairness and security control, and requires a sound governance framework to ensure that technological development is ethical and regulatory."]}}',
+ page_index: 9,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784214771,
+ },
+ {
+ id: '1756784214809_487052f4',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '4e1688e7-ddcc-40ff-b5ed-763b37d1e9bf',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 9 Page production completed**\\n📄 **Page Title**: Development challenges and responses\\n📝 **Page Content Outline**:\\n 1. Development challenges and responses\\n 2. Technical challenges\\n 3. Large model training requires significant computing resources and data support, as well as continuous optimization of algorithm efficiency, reduced training costs, enhanced model performance and extensive capability.\\n 4. Market competition\\n 5. AILarge model markets are highly competitive and require a leading advantage in technological innovation, product differentiation and ecological construction, as well as sustainable barriers to competition.\\n 6. Compliance and security\\n 7. AITechnology development faces challenges such as data privacy, algorithm fairness and security control, and requires a sound governance framework to ensure that technological development is ethical and regulatory.\\n📊 **Statistics**: 7 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 9, "template_page_index": 9, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '7cfe03f6-5371-4056-9ee3-2c0b18c1639c',
+ tool_call_id: 'tooluse_g0VEqsMsQXODy2VjFdk32A',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784214816,
+ },
+ {
+ id: '1756784222860_feb23ef8',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '0552d6c4-81bb-412c-b8a6-c60ec0ad931f',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 10,
+ replace_content:
+ '{"text": {"5144e569": ["The future of 01.AI."], "db5ff530": ["Technology road map"], "2e7e6eec": ["Continuously advance large model technological innovation, exploring the front lines of polymodes, context, reasoning capabilities, and building strongerAIInfrastructure."], "7f245244": ["Market outreach strategy"], "bdbde1cb": ["Deepening industrial applications, expanding international market layouts and building global economic growthAIService network, asAI 2.0The leader of the times."], "0a629dfc": ["Vision for ecological cooperation"], "e2197e6e": ["Building with more partnersAIEcology, promoting open-source community development and promotingAITechnology inclusion, enabling the digital transformation of society as a whole."]}}',
+ page_index: 10,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784226359,
+ },
+ {
+ id: '1756784226380_577992a0',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '0552d6c4-81bb-412c-b8a6-c60ec0ad931f',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 10 Page production completed**\\n📄 **Page Title**: The future of 01.AI.\\n📝 **Page Content Outline**:\\n 1. The future of 01.AI.\\n 2. Technology road map\\n 3. Continuously advance large model technological innovation, exploring the front lines of polymodes, context, reasoning capabilities, and building strongerAIInfrastructure.\\n 4. Market outreach strategy\\n 5. Deepening industrial applications, expanding international market layouts and building global economic growthAIService network, asAI 2.0The leader of the times.\\n 6. Vision for ecological cooperation\\n 7. Building with more partnersAIEcology, promoting open-source community development and promotingAITechnology inclusion, enabling the digital transformation of society as a whole.\\n📊 **Statistics**: 7 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 10, "template_page_index": 10, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '106fe375-6661-4291-92c0-277d17548076',
+ tool_call_id: 'tooluse_SV_kyYnoRCSVbe7OdmqxUQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784226405,
+ },
+ {
+ id: '1756784234209_1dbc848a',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: 'e81bdf1e-3c53-4b28-9d7a-1ee22b48fc94',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 11,
+ replace_content:
+ '{"text": {"61d550f6": ["Summary and outlook"], "86dba575": ["The core values of 01.AI."], "f4abbd51": ["# And the whole thing #"Zero One."It is a reputation for innovation that goes from nothing to nothing, from simplicity to complexity.AI 2.0Technological revolution, throughYiThe series of large models promotes industrial intelligence and creates great economic and social values."], "4428e62e": ["AI 2.0The mission of the times"], "cd06c39d": ["Yes.AI 2.0The age, when all things will continue to be driven by the idea of open cooperation.AITechnology is inclusive, helping to build a smart society, andAITechnology truly serves the well-being of humankind."]}}',
+ page_index: 11,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784234215,
+ },
+ {
+ id: '1756784239361_95f0ca27',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '090969e4-9556-4d0d-a871-eb2d5721d28c',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 11,
+ replace_content:
+ '{"text": {"61d550f6": ["Summary and outlook"], "86dba575": ["The core values of 01.AI."], "f4abbd51": ["The zero-zero-one name is the expression of innovative ideas ranging from nothing to nothing, from simplicity to complexity.AI 2.0Technological revolution, throughYiThe series of large models promotes industrial intelligence and creates great economic and social values."], "4428e62e": ["AI 2.0The mission of the times"], "cd06c39d": ["Yes.AI 2.0The age, when all things will continue to be driven by the idea of open cooperation.AITechnology is inclusive, helping to build a smart society, andAITechnology truly serves the well-being of humankind."]}}',
+ page_index: 11,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784242808,
+ },
+ {
+ id: '1756784242845_e88252c9',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '090969e4-9556-4d0d-a871-eb2d5721d28c',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 11 Page production completed**\\n📄 **Page Title**: Summary and outlook\\n📝 **Page Content Outline**:\\n 1. Summary and outlook\\n 2. The core values of 01.AI.\\n 3. The zero-zero-one name is the expression of innovative ideas ranging from nothing to nothing, from simplicity to complexity.AI 2.0Technological revolution, throughYiThe series of large models promotes industrial intelligence and creates great economic and social values.\\n 4. AI 2.0The mission of the times\\n 5. Yes.AI 2.0The age, when all things will continue to be driven by the idea of open cooperation.AITechnology is inclusive, helping to build a smart society, andAITechnology truly serves the well-being of humankind.\\n📊 **Statistics**: 5 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 11, "template_page_index": 11, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: 'de7221b5-072e-460f-b26c-f785923b5125',
+ tool_call_id: 'tooluse_KQp5kPJ6T8u4Fj2fHj1XWA',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784242857,
+ },
+ {
+ id: '1756784247554_73e24605',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '1ed334f0-c276-41fe-98b5-e0e6318209a1',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 12,
+ replace_content: '{"text": {"33017331": ["Thank you."]}}',
+ page_index: 12,
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ timestamp: 1756784250937,
+ },
+ {
+ id: '1756784250973_ba5dd9c6',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'make_new_slide_from_template',
+ run_id: '1ed334f0-c276-41fe-98b5-e0e6318209a1',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 12 Page production completed**\\n📄 **Page Title**: Thank you.\\n📝 **Page Content Outline**:\\n 1. Thank you.\\n📊 **Statistics**: 1 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 12, "template_page_index": 12, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: '19312131-4c4b-44f4-9b71-cf1915f1c476',
+ tool_call_id: 'tooluse_0BfBU1u1RAWpi-8hBIby3A',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784250981,
+ },
+ {
+ id: '1756784255624_c41cbcf0',
+ role: 'assistant',
+ type: 'plan_update',
+ content: 'Note on planned advancement',
+ detail: {
+ action: 'update',
+ steps: [
+ {
+ id: '4',
+ status: 'success',
+ started_at: 1756784255624,
+ },
+ {
+ id: '5',
+ status: 'running',
+ started_at: 1756784255624,
+ },
+ ],
+ },
+ timestamp: 1756784255628,
+ },
+ {
+ id: '1756784259934_285cadd8',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '4cf3c72d-fe43-45a5-b70f-5fe05cd300a0',
+ tool: 'upload_ppt_to_sandbox',
+ status: 'pending',
+ param: {
+ ppt_file_path: 'auto',
+ sandbox_file_path: '/workspace/The company introduced 01.AI..pptx',
+ },
+ action: 'Calling upload_ppt_to_sandbox',
+ action_content: '',
+ },
+ timestamp: 1756784266310,
+ },
+ {
+ id: '1756784275632_485a0e6c',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'upload_ppt_to_sandbox',
+ run_id: '4cf3c72d-fe43-45a5-b70f-5fe05cd300a0',
+ result: {
+ content:
+ '{"content": "🎉 File upload complete\\n\\n📄 Successfully uploaded files:\\n- PPTX: PowerPointDocumentation (Editable Presentation)\\n- PDF: PDFDocumentation (Easy to share and view)\\n\\n🔗 Download:\\n- Please click on the file browsing first, then click the top right download button\\n\\n📍 File Location:\\n- PPTXPath: /workspace/The company introduced 01.AI..pptx\\n- PDFPath: /workspace/The company introduced 01.AI..pdf\\n\\n✨ The document is ready for downloading directly.", "content_type": "markdown", "status": "success", "successful_files": ["PPTX", "PDF"], "failed_files": [], "workspace_paths": ["/workspace/The company introduced 01.AI..pptx", "/workspace/The company introduced 01.AI..pdf"], "attachments_for_message_notify_user": ["/workspace/The company introduced 01.AI..pptx", "/workspace/The company introduced 01.AI..pdf"], "pptx_url": "https://storage.example.invalid/files/FILE_EXAMPLE_01.pptx", "pptx_path": "/workspace/The company introduced 01.AI..pptx", "pdf_url": "https://storage.example.invalid/files/FILE_EXAMPLE_02.pdf", "pdf_path": "/workspace/The company introduced 01.AI..pdf"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'upload_ppt_to_sandbox',
+ id: null,
+ tool_call_id: 'tooluse_DjGSZmq4SU20tUEA6HFXrQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784275647,
+ },
+ {
+ id: '1756784299818_1cc3774d',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ '🎉 The company introduced 01.AI.PPTProduction complete!\n\nI\'ve successfully created a comprehensive introduction to the 101th Century.PPT,Total12Pages\n\n📋 **PPTOverview of content:**\n1. **Cover Page** - The company introduced 01.AI.\n2. **Contents Page** - PresentationPPTStructure (corporate profiles, core technologies, product applications, future outlook)\n3. **Corporate profile** - Basic Introduction and Development Background\n4. **AI 2.0Platform vision** - Technological innovation, open source ecology, commercial applications, future layout\n5. **One technology philosophy.** - Zero-one.AIof the Convention on the Elimination of All Forms of Discrimination against Women\n6. **YiSeries of large model technical architecture** - Model parameters, multi-modular capabilities, context processing\n7. **The product ecology of 10th.** - Business solutions, open source ecology, industry applications\n8. **Commercial value** - Industrial smartening and ecosystem building\n9. **Development challenges and responses** - Technical challenges, market competition, compliance security\n10. **Future development planning** - Technology road map, market expansion, ecological cooperation\n11. **Summary and outlook** - Core values andAI 2.0The mission of the times\n12. **End Page** - Thank you.\n\n✨ **Feature highlights:**\n- Use your supply"01.AI.-Light"Template Design\n- The content covers the corporate context, technical characteristics, product systems, business values.\n- Based on the latest information on the 1010th of all.YiSeries of large model techniques\n- Professional visual design and logical content structure\n\n📁 **File format:**\n- PPTXFormat: EditablePowerPointDocumentation\n- PDFFormat: Easy to share and viewPDFDocumentation\n\nYou can download it directly. If you need to change, please tell me what you want to adjust!',
+ detail: {
+ attachments: [
+ {
+ filename: 'The company introduced 01.AI..pptx',
+ path: '/workspace/The company introduced 01.AI..pptx',
+ url: 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_05.pptx',
+ size: 9226095,
+ content_type: 'application/octet-stream',
+ show_user: 1,
+ },
+ {
+ filename: 'The company introduced 01.AI..pdf',
+ path: '/workspace/The company introduced 01.AI..pdf',
+ url: 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_06.pdf',
+ size: 9317941,
+ content_type: 'application/pdf',
+ show_user: 1,
+ },
+ ],
+ intent_type: 'task_completed',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784299837,
+ },
+ {
+ id: '1756784305117_54f38255',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: 'e6deb061-47f9-48c0-8095-78e79ce1359a',
+ tool: 'agent_end_task',
+ status: 'pending',
+ param: {},
+ action: 'Calling agent_end_task',
+ action_content: '',
+ },
+ timestamp: 1756784305121,
+ },
+ {
+ id: '1756784305126_a132dc95',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'agent_end_task',
+ run_id: 'e6deb061-47f9-48c0-8095-78e79ce1359a',
+ result: {
+ content: 'Task formally ended',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'agent_end_task',
+ id: '3a16a6ce-03c8-4177-bd5b-53529cfd2e16',
+ tool_call_id: 'tooluse_DFtCP1ySReKEondV_ClzBw',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1756784305129,
+ },
+ {
+ id: '1756784305134_910d50f9',
+ role: 'assistant',
+ type: 'finish_reason',
+ content: 'Task finished',
+ detail: {
+ reason: 'Task completed',
+ status: 'completed',
+ },
+ timestamp: 1756784305136,
+ },
+];
+
+export const videoChunk = {
+ id: 39642,
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ tool: 'get_video_progress',
+ run_id: 'cb9b80eb-1aab-4b83-a96e-8ecca71e8971',
+ result: {
+ content:
+ '{"content":"https://storage.example.invalid/videos/VIDEO_EXAMPLE.mp4","content_type":"video"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'get_video_progress',
+ id: null,
+ tool_call_id: 'tooluse_ifAG1C6YTjiX2PeJOFg7qA',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ step_id: null,
+ timestamp: 1758099782,
+ is_llm_message: false,
+ session_id: '4e563009e5404d01',
+};
+
+
+
+export const streamingChunks = [
+ {
+ id: '1762847684106_a2fdd9db',
+ role: 'user',
+ type: 'text',
+ content:
+ 'Yes. 3 Plants (in thousands of euros)A/B/C)and 3 Warehouses (in United States dollars)X/Y/Z),A Present. X Freight 10 Dollar / Tons, arriving Y15 Dollar / Tons, arriving Z20 Dollar / (a) tons;B Present. X12 Dollar / Tons, arriving Y13 Dollar / Tons, arriving Z18 Dollar / (a) tons;C Present. X14 Dollar / Tons, arriving Y16 Dollar / Tons, arriving Z17 Dollar / Tons.A Most produced daily 200 Tons,B Most 150 Tons,C Most 250 (a) tons;X Day requirements 180 Tons,Y Requirements 220 Tons,Z Requirements 100 How do you distribute traffic so that total freight costs are minimal and all supply and demand are met?',
+ detail: {},
+ timestamp: 1762847684107,
+ },
+ {
+ id: '1762847688375_5viz',
+ role: 'assistant',
+ type: 'text',
+ content: 'Here.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688375,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688376_b76x',
+ role: 'assistant',
+ type: 'text',
+ content: 'It\'s a book.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688376,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688377_xym0',
+ role: 'assistant',
+ type: 'text',
+ content: 'Type**',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688377,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688378_kiv4',
+ role: 'assistant',
+ type: 'text',
+ content: 'Transport',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688378,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688378_9od6',
+ role: 'assistant',
+ type: 'text',
+ content: 'Loser question.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688378,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688379_pfic',
+ role: 'assistant',
+ type: 'text',
+ content: 'Topic**(',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688379,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688379_krbo',
+ role: 'assistant',
+ type: 'text',
+ content: 'Transportation',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688379,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688380_9ite',
+ role: 'assistant',
+ type: 'text',
+ content: ' Problem),belong',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688380,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688380_ajba',
+ role: 'assistant',
+ type: 'text',
+ content: 'On Line',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688380,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688380_6szk',
+ role: 'assistant',
+ type: 'text',
+ content: 'Sex planning. I...',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688380,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688381_1337',
+ role: 'assistant',
+ type: 'text',
+ content: 'Come on.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688381,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847688381_lkj9',
+ role: 'assistant',
+ type: 'text',
+ content: 'Help you.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847688381,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689120_mwev',
+ role: 'assistant',
+ type: 'text',
+ content: 'Construction',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847689120,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689121_6klb',
+ role: 'assistant',
+ type: 'text',
+ content: 'Stereo.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847689121,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689122_ny5c',
+ role: 'assistant',
+ type: 'text',
+ content: 'Type',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847689122,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689123_g1c1',
+ role: 'assistant',
+ type: 'text',
+ content: 'Undo the best.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847689123,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689123_lpbr',
+ role: 'assistant',
+ type: 'text',
+ content: 'Case.',
+ detail: {
+ streaming: true,
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ },
+ timestamp: 1762847689123,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689534_5xaq',
+ role: 'assistant',
+ type: 'text',
+ content: '',
+ detail: {
+ run_id: '63c5826f-234b-49f3-8f8d-a06df6fe897b',
+ full_content:
+ 'It\'s a classic.**Transport issues**(Transportation Problem),It\'s a linear planning problem. I\'ll help you build models and solve the best solutions.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Establishment of mathematical models and constraints on transport',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'UsePythonWrite linear planning solver codes',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Generate visualization and detailed analysis',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'CreateExcelShow the best option in the table',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'toolu_bdrk_01JXyrUCH8gNjAUBaNcRRKne',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 0,
+ output_tokens: 228,
+ total_tokens: 228,
+ input_token_details: {},
+ },
+ },
+ timestamp: 1762847689534,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847689574_wl7w',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will work according to the following plan:\n\n1. Establishment of mathematical models and constraints on transport\n2. UsePythonWrite linear planning solver codes\n3. Generate visualization and detailed analysis\n4. CreateExcelShow the best option in the table\n\nDuring my work, you can interrupt me at any time to provide new information or adjust the plan.',
+ detail: {},
+ timestamp: 1762847689574,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ },
+ {
+ id: '1762847689574_ukog',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Establishment of mathematical models and constraints on transport',
+ status: 'running',
+ started_at: 1762847689574,
+ },
+ {
+ id: '2',
+ title: 'UsePythonWrite linear planning solver codes',
+ status: 'pending',
+ started_at: 1762847689574,
+ },
+ {
+ id: '3',
+ title: 'Generate visualization and detailed analysis',
+ status: 'pending',
+ started_at: 1762847689574,
+ },
+ {
+ id: '4',
+ title: 'CreateExcelShow the best option in the table',
+ status: 'pending',
+ started_at: 1762847689574,
+ },
+ ],
+ },
+ timestamp: 1762847689574,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847692619_f8gt',
+ role: 'assistant',
+ type: 'text',
+ content: 'Current',
+ detail: {
+ streaming: true,
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ },
+ timestamp: 1762847692620,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847692621_ik31',
+ role: 'assistant',
+ type: 'text',
+ content: '- It\'s Jean.',
+ detail: {
+ streaming: true,
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ },
+ timestamp: 1762847692621,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847692621_69yk',
+ role: 'assistant',
+ type: 'text',
+ content: 'I\'ll use it.',
+ detail: {
+ streaming: true,
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ },
+ timestamp: 1762847692621,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847692622_hyeh',
+ role: 'assistant',
+ type: 'text',
+ content: 'PythonPlease.',
+ detail: {
+ streaming: true,
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ },
+ timestamp: 1762847692622,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847692623_ea20',
+ role: 'assistant',
+ type: 'text',
+ content: '- I\'m sorry.',
+ detail: {
+ streaming: true,
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ },
+ timestamp: 1762847692623,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847692624_t624',
+ role: 'assistant',
+ type: 'text',
+ content: 'Subject:',
+ detail: {
+ streaming: true,
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ },
+ timestamp: 1762847692624,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847702624_0650df80',
+ role: 'assistant',
+ type: 'live_status',
+ content: 'Processing...',
+ timestamp: 1762847702624,
+ session_id: 'b405c8032f5c4423',
+ },
+ {
+ id: '1762847704960_u0hu',
+ role: 'assistant',
+ type: 'text',
+ content: '',
+ detail: {
+ run_id: '4f18bc77-4e58-487a-8b19-006485cfbe6f',
+ full_content: 'Now let me use it.PythonThe question is:',
+ tool_calls: [
+ {
+ name: 'write_file',
+ args: {
+ path: '/workspace/transport_optimization.py',
+ content:
+ "\"\"\"\nTransport solver - Linear planning\n3A factory.3Best Transport Distribution Programme for Warehouses\n\"\"\"\n\nimport numpy as np\nfrom scipy.optimize import linprog\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# Set up Chinese fonts\nplt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei']\nplt.rcParams['axes.unicode_minus'] = False\n\n# ============ Problem data ============\n# Plant production (tons)/ God \nsupply = {'A': 200, 'B': 150, 'C': 250}\nfactories = list(supply.keys())\n\n# Warehouse requirements (tons)/ God \ndemand = {'X': 180, 'Y': 220, 'Z': 100}\nwarehouses = list(demand.keys())\n\n# Freight matrix (in US$)/tons)\n# Line: PlantA、B、C;Column: WarehouseX、Y、Z\ncost_matrix = np.array([\n [10, 15, 20], # PlantA\n [12, 13, 18], # PlantB\n [14, 16, 17] # PlantC\n])\n\nprint(\"=\" * 60)\nprint(\"Transport solver\")\nprint(\"=\" * 60)\nprint(\"\\n【Problem data\")\nprint(f\"Plant Production: {supply}\")\nprint(f\"Warehouse requirements: {demand}\")\nprint(f\"\\nFreight matrix (in US$)/tons):\")\ncost_df = pd.DataFrame(cost_matrix, index=factories, columns=warehouses)\nprint(cost_df)\n\n# ============ Linear Plan Solution ============\n# Decision variables:x_ij It means from the factory.iTo the warehouse.jof transport\n# Total9variable, in order:x_AX, x_AY, x_AZ, x_BX, x_BY, x_BZ, x_CX, x_CY, x_CZ\n\n# Target function factor (freight)\nc = cost_matrix.flatten() # Convert to 1-D array\n\n# Impansive constraints:Ax <= b\n# 1. Plant Production Constraints (Supplier Constraints)\nA_ub = []\nb_ub = []\n\n# PlantA: x_AX + x_AY + x_AZ <= 200\nA_ub.append([1, 1, 1, 0, 0, 0, 0, 0, 0])\nb_ub.append(supply['A'])\n\n# PlantB: x_BX + x_BY + x_BZ <= 150\nA_ub.append([0, 0, 0, 1, 1, 1, 0, 0, 0])\nb_ub.append(supply['B'])\n\n# PlantC: x_CX + x_CY + x_CZ <= 250\nA_ub.append([0, 0, 0, 0, 0, 0, 1, 1, 1])\nb_ub.append(supply['C'])\n\nA_ub = np.array(A_ub)\nb_ub = np.array(b_ub)\n\n# Equivalent:Aeq x = beq\n# Warehouse requirements constraint (must be met)\nA_eq = []\nb_eq = []\n\n# WarehouseX: x_AX + x_BX + x_CX = 180\nA_eq.append([1, 0, 0, 1, 0, 0, 1, 0, 0])\nb_eq.append(demand['X'])\n\n# WarehouseY: x_AY + x_BY + x_CY = 220\nA_eq.append([0, 1, 0, 0, 1, 0, 0, 1, 0])\nb_eq.append(demand['Y'])\n\n# WarehouseZ: x_AZ + x_BZ + x_CZ = 100\nA_eq.append([0, 0, 1, 0, 0, 1, 0, 0, 1])\nb_eq.append(demand['Z'])\n\nA_eq = np.array(A_eq)\nb_eq = np.array(b_eq)\n\n# Variable boundary: all traffic >= 0\nbounds = [(0, None) for _ in range(9)]\n\n# Solver\nresult = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, \n bounds=bounds, method='highs')\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"【\"Solver results.\"\")\nprint(\"=\" * 60)\n\nif result.success:\n print(f\"\\n✓ Solution succeeded!\")\n print(f\"Minimum total freight: {result.fun:.2f} Dollar\")\n \n # Reorder the result as a matrix\n solution = result.x.reshape(3, 3)\n solution_df = pd.DataFrame(solution, index=factories, columns=warehouses)\n \n print(f\"\\nBest Transport Program (tons):\")\n print(solution_df.round(2))\n \n # Certification constraints\n print(f\"\\n【Other Organiser\")\n print(f\"Plant Production Inspection:\")\n for i, factory in enumerate(factories):\n total = solution[i].sum()\n print(f\" {factory}: {total:.2f} tons (Production: {supply[factory]} tons) {'✓' if total <= supply[factory] + 1e-6 else '✗'}\")\n \n print(f\"\\nWarehouse requirements check:\")\n for j, warehouse in enumerate(warehouses):\n total = solution[:, j].sum()\n print(f\" {warehouse}: {total:.2f} tons (Requirements: {demand[warehouse]} tons) {'✓' if abs(total - demand[warehouse]) < 1e-6 else '✗'}\")\n \n # Calculation of freight costs for each route\n print(f\"\\n【 All routes are fined \")\n total_cost = 0\n for i, factory in enumerate(factories):\n for j, warehouse in enumerate(warehouses):\n qty = solution[i, j]\n if qty > 1e-6: # Only the routes of transport are shown\n route_cost = qty * cost_matrix[i, j]\n total_cost += route_cost\n print(f\" {factory} → {warehouse}: {qty:.2f} tons × {cost_matrix[i, j]} Dollar/tons = {route_cost:.2f} Dollar\")\n \n print(f\"\\nTotal freight: {total_cost:.2f} Dollar\")\n \n # Save result toCSV\n solution_df.to_csv('/workspace/transport_solution.csv')\n print(f\"\\n✓ Results saved to transport_solution.csv\")\n \nelse:\n print(f\"✗ Solve failed: {result.message}\")\n\n# ============ Data Export forExcel ============\n# Create detailed results table\nresult_data = {\n 'Plant': [],\n 'WarehouseX': [],\n 'WarehouseY': [],\n 'WarehouseZ': [],\n 'Total Production': [],\n 'Production': []\n}\n\nfor i, factory in enumerate(factories):\n result_data['Plant'].append(factory)\n result_data['WarehouseX'].append(f\"{solution[i, 0]:.2f}\")\n result_data['WarehouseY'].append(f\"{solution[i, 1]:.2f}\")\n result_data['WarehouseZ'].append(f\"{solution[i, 2]:.2f}\")\n result_data['Total Production'].append(f\"{solution[i].sum():.2f}\")\n result_data['Production'].append(supply[factory])\n\nresult_df = pd.DataFrame(result_data)\nresult_df.to_csv('/workspace/transport_detail.csv', index=False)\n\nprint(\"\\n\" + \"=\" * 60)\n",
+ },
+ id: 'toolu_bdrk_01TE2uRFPjayQeiq3p18XGr3',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 0,
+ output_tokens: 2248,
+ total_tokens: 2248,
+ input_token_details: {},
+ },
+ },
+ timestamp: 1762847704960,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847705017_pkxg',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '2773a541-541b-4233-848e-e3bd164474bd',
+ tool: 'write_file',
+ status: 'pending',
+ param: {
+ path: '/workspace/transport_optimization.py',
+ content:
+ "\"\"\"\nTransport solver - Linear planning\n3A factory.3Best Transport Distribution Programme for Warehouses\n\"\"\"\n\nimport numpy as np\nfrom scipy.optimize import linprog\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\n# Set up Chinese fonts\nplt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei']\nplt.rcParams['axes.unicode_minus'] = False\n\n# ============ Problem data ============\n# Plant production (tons)/ God \nsupply = {'A': 200, 'B': 150, 'C': 250}\nfactories = list(supply.keys())\n\n# Warehouse requirements (tons)/ God \ndemand = {'X': 180, 'Y': 220, 'Z': 100}\nwarehouses = list(demand.keys())\n\n# Freight matrix (in US$)/tons)\n# Line: PlantA、B、C;Column: WarehouseX、Y、Z\ncost_matrix = np.array([\n [10, 15, 20], # PlantA\n [12, 13, 18], # PlantB\n [14, 16, 17] # PlantC\n])\n\nprint(\"=\" * 60)\nprint(\"Transport solver\")\nprint(\"=\" * 60)\nprint(\"\\n【Problem data\")\nprint(f\"Plant Production: {supply}\")\nprint(f\"Warehouse requirements: {demand}\")\nprint(f\"\\nFreight matrix (in US$)/tons):\")\ncost_df = pd.DataFrame(cost_matrix, index=factories, columns=warehouses)\nprint(cost_df)\n\n# ============ Linear Plan Solution ============\n# Decision variables:x_ij It means from the factory.iTo the warehouse.jof transport\n# Total9variable, in order:x_AX, x_AY, x_AZ, x_BX, x_BY, x_BZ, x_CX, x_CY, x_CZ\n\n# Target function factor (freight)\nc = cost_matrix.flatten() # Convert to 1-D array\n\n# Impansive constraints:Ax <= b\n# 1. Plant Production Constraints (Supplier Constraints)\nA_ub = []\nb_ub = []\n\n# PlantA: x_AX + x_AY + x_AZ <= 200\nA_ub.append([1, 1, 1, 0, 0, 0, 0, 0, 0])\nb_ub.append(supply['A'])\n\n# PlantB: x_BX + x_BY + x_BZ <= 150\nA_ub.append([0, 0, 0, 1, 1, 1, 0, 0, 0])\nb_ub.append(supply['B'])\n\n# PlantC: x_CX + x_CY + x_CZ <= 250\nA_ub.append([0, 0, 0, 0, 0, 0, 1, 1, 1])\nb_ub.append(supply['C'])\n\nA_ub = np.array(A_ub)\nb_ub = np.array(b_ub)\n\n# Equivalent:Aeq x = beq\n# Warehouse requirements constraint (must be met)\nA_eq = []\nb_eq = []\n\n# WarehouseX: x_AX + x_BX + x_CX = 180\nA_eq.append([1, 0, 0, 1, 0, 0, 1, 0, 0])\nb_eq.append(demand['X'])\n\n# WarehouseY: x_AY + x_BY + x_CY = 220\nA_eq.append([0, 1, 0, 0, 1, 0, 0, 1, 0])\nb_eq.append(demand['Y'])\n\n# WarehouseZ: x_AZ + x_BZ + x_CZ = 100\nA_eq.append([0, 0, 1, 0, 0, 1, 0, 0, 1])\nb_eq.append(demand['Z'])\n\nA_eq = np.array(A_eq)\nb_eq = np.array(b_eq)\n\n# Variable boundary: all traffic >= 0\nbounds = [(0, None) for _ in range(9)]\n\n# Solver\nresult = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, \n bounds=bounds, method='highs')\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"【\"Solver results.\"\")\nprint(\"=\" * 60)\n\nif result.success:\n print(f\"\\n✓ Solution succeeded!\")\n print(f\"Minimum total freight: {result.fun:.2f} Dollar\")\n \n # Reorder the result as a matrix\n solution = result.x.reshape(3, 3)\n solution_df = pd.DataFrame(solution, index=factories, columns=warehouses)\n \n print(f\"\\nBest Transport Program (tons):\")\n print(solution_df.round(2))\n \n # Certification constraints\n print(f\"\\n【Other Organiser\")\n print(f\"Plant Production Inspection:\")\n for i, factory in enumerate(factories):\n total = solution[i].sum()\n print(f\" {factory}: {total:.2f} tons (Production: {supply[factory]} tons) {'✓' if total <= supply[factory] + 1e-6 else '✗'}\")\n \n print(f\"\\nWarehouse requirements check:\")\n for j, warehouse in enumerate(warehouses):\n total = solution[:, j].sum()\n print(f\" {warehouse}: {total:.2f} tons (Requirements: {demand[warehouse]} tons) {'✓' if abs(total - demand[warehouse]) < 1e-6 else '✗'}\")\n \n # Calculation of freight costs for each route\n print(f\"\\n【 All routes are fined \")\n total_cost = 0\n for i, factory in enumerate(factories):\n for j, warehouse in enumerate(warehouses):\n qty = solution[i, j]\n if qty > 1e-6: # Only the routes of transport are shown\n route_cost = qty * cost_matrix[i, j]\n total_cost += route_cost\n print(f\" {factory} → {warehouse}: {qty:.2f} tons × {cost_matrix[i, j]} Dollar/tons = {route_cost:.2f} Dollar\")\n \n print(f\"\\nTotal freight: {total_cost:.2f} Dollar\")\n \n # Save result toCSV\n solution_df.to_csv('/workspace/transport_solution.csv')\n print(f\"\\n✓ Results saved to transport_solution.csv\")\n \nelse:\n print(f\"✗ Solve failed: {result.message}\")\n\n# ============ Data Export forExcel ============\n# Create detailed results table\nresult_data = {\n 'Plant': [],\n 'WarehouseX': [],\n 'WarehouseY': [],\n 'WarehouseZ': [],\n 'Total Production': [],\n 'Production': []\n}\n\nfor i, factory in enumerate(factories):\n result_data['Plant'].append(factory)\n result_data['WarehouseX'].append(f\"{solution[i, 0]:.2f}\")\n result_data['WarehouseY'].append(f\"{solution[i, 1]:.2f}\")\n result_data['WarehouseZ'].append(f\"{solution[i, 2]:.2f}\")\n result_data['Total Production'].append(f\"{solution[i].sum():.2f}\")\n result_data['Production'].append(supply[factory])\n\nresult_df = pd.DataFrame(result_data)\nresult_df.to_csv('/workspace/transport_detail.csv', index=False)\n\nprint(\"\\n\" + \"=\" * 60)\n",
+ },
+ action: 'Calling write_file',
+ action_content: '',
+ },
+ timestamp: 1762847705017,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847706230_8omp',
+ role: 'inner_message',
+ type: 'config',
+ content: 'update_session',
+ detail: {
+ session_id: 'b405c8032f5c4423',
+ sandbox_id: 'irqrqm82v5cy05inqn2mn-0b1eea18',
+ sandbox_url: '',
+ },
+ timestamp: 1762847706230,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1762847706403_bq9o',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ run_id: '2773a541-541b-4233-848e-e3bd164474bd',
+ tool: 'write_file',
+ result: {
+ content:
+ '{"message": "Successfully wrote to file: /workspace/transport_optimization.py", "old_file_content": "", "new_file_content": "\\"\\"\\"\\nTransport solver - Linear planning\\n3A factory.3Best Transport Distribution Programme for Warehouses\\n\\"\\"\\"\\n\\nimport numpy as np\\nfrom scipy.optimize import linprog\\nimport pandas as pd\\nimport matplotlib.pyplot as plt\\n\\n# Set up Chinese fonts\\nplt.rcParams[\'font.sans-serif\'] = [\'WenQuanYi Micro Hei\']\\nplt.rcParams[\'axes.unicode_minus\'] = False\\n\\n# ============ Problem data ============\\n# Plant production (tons)/ God \\nsupply = {\'A\': 200, \'B\': 150, \'C\': 250}\\nfactories = list(supply.keys())\\n\\n# Warehouse requirements (tons)/ God \\ndemand = {\'X\': 180, \'Y\': 220, \'Z\': 100}\\nwarehouses = list(demand.keys())\\n\\n# Freight matrix (in US$)/tons)\\n# Line: PlantA、B、C;Column: WarehouseX、Y、Z\\ncost_matrix = np.array([\\n [10, 15, 20], # PlantA\\n [12, 13, 18], # PlantB\\n [14, 16, 17] # PlantC\\n])\\n\\nprint(\\"=\\" * 60)\\nprint(\\"Transport solver\\")\\nprint(\\"=\\" * 60)\\nprint(\\"\\\\n【Problem data\\")\\nprint(f\\"Plant Production: {supply}\\")\\nprint(f\\"Warehouse requirements: {demand}\\")\\nprint(f\\"\\\\nFreight matrix (in US$)/tons):\\")\\ncost_df = pd.DataFrame(cost_matrix, index=factories, columns=warehouses)\\nprint(cost_df)\\n\\n# ============ Linear Plan Solution ============\\n# Decision variables:x_ij It means from the factory.iTo the warehouse.jof transport\\n# Total9variable, in order:x_AX, x_AY, x_AZ, x_BX, x_BY, x_BZ, x_CX, x_CY, x_CZ\\n\\n# Target function factor (freight)\\nc = cost_matrix.flatten() # Convert to 1-D array\\n\\n# Impansive constraints:Ax <= b\\n# 1. Plant Production Constraints (Supplier Constraints)\\nA_ub = []\\nb_ub = []\\n\\n# PlantA: x_AX + x_AY + x_AZ <= 200\\nA_ub.append([1, 1, 1, 0, 0, 0, 0, 0, 0])\\nb_ub.append(supply[\'A\'])\\n\\n# PlantB: x_BX + x_BY + x_BZ <= 150\\nA_ub.append([0, 0, 0, 1, 1, 1, 0, 0, 0])\\nb_ub.append(supply[\'B\'])\\n\\n# PlantC: x_CX + x_CY + x_CZ <= 250\\nA_ub.append([0, 0, 0, 0, 0, 0, 1, 1, 1])\\nb_ub.append(supply[\'C\'])\\n\\nA_ub = np.array(A_ub)\\nb_ub = np.array(b_ub)\\n\\n# Equivalent:Aeq x = beq\\n# Warehouse requirements constraint (must be met)\\nA_eq = []\\nb_eq = []\\n\\n# WarehouseX: x_AX + x_BX + x_CX = 180\\nA_eq.append([1, 0, 0, 1, 0, 0, 1, 0, 0])\\nb_eq.append(demand[\'X\'])\\n\\n# WarehouseY: x_AY + x_BY + x_CY = 220\\nA_eq.append([0, 1, 0, 0, 1, 0, 0, 1, 0])\\nb_eq.append(demand[\'Y\'])\\n\\n# WarehouseZ: x_AZ + x_BZ + x_CZ = 100\\nA_eq.append([0, 0, 1, 0, 0, 1, 0, 0, 1])\\nb_eq.append(demand[\'Z\'])\\n\\nA_eq = np.array(A_eq)\\nb_eq = np.array(b_eq)\\n\\n# Variable boundary: all traffic >= 0\\nbounds = [(0, None) for _ in range(9)]\\n\\n# Solver\\nresult = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, \\n bounds=bounds, method=\'highs\')\\n\\nprint(\\"\\\\n\\" + \\"=\\" * 60)\\nprint(\\"【"Solver results."\\")\\nprint(\\"=\\" * 60)\\n\\nif result.success:\\n print(f\\"\\\\n✓ Solution succeeded!\\")\\n print(f\\"Minimum total freight: {result.fun:.2f} Dollar\\")\\n \\n # Reorder the result as a matrix\\n solution = result.x.reshape(3, 3)\\n solution_df = pd.DataFrame(solution, index=factories, columns=warehouses)\\n \\n print(f\\"\\\\nBest Transport Program (tons):\\")\\n print(solution_df.round(2))\\n \\n # Certification constraints\\n print(f\\"\\\\n【Other Organiser\\")\\n print(f\\"Plant Production Inspection:\\")\\n for i, factory in enumerate(factories):\\n total = solution[i].sum()\\n print(f\\" {factory}: {total:.2f} tons (Production: {supply[factory]} tons) {\'✓\' if total <= supply[factory] + 1e-6 else \'✗\'}\\")\\n \\n print(f\\"\\\\nWarehouse requirements check:\\")\\n for j, warehouse in enumerate(warehouses):\\n total = solution[:, j].sum()\\n print(f\\" {warehouse}: {total:.2f} tons (Requirements: {demand[warehouse]} tons) {\'✓\' if abs(total - demand[warehouse]) < 1e-6 else \'✗\'}\\")\\n \\n # Calculation of freight costs for each route\\n print(f\\"\\\\n【 All routes are fined \\")\\n total_cost = 0\\n for i, factory in enumerate(factories):\\n for j, warehouse in enumerate(warehouses):\\n qty = solution[i, j]\\n if qty > 1e-6: # Only the routes of transport are shown\\n route_cost = qty * cost_matrix[i, j]\\n total_cost += route_cost\\n print(f\\" {factory} → {warehouse}: {qty:.2f} tons × {cost_matrix[i, j]} Dollar/tons = {route_cost:.2f} Dollar\\")\\n \\n print(f\\"\\\\nTotal freight: {total_cost:.2f} Dollar\\")\\n \\n # Save result toCSV\\n solution_df.to_csv(\'/workspace/transport_solution.csv\')\\n print(f\\"\\\\n✓ Results saved to transport_solution.csv\\")\\n \\nelse:\\n print(f\\"✗ Solve failed: {result.message}\\")\\n\\n# ============ Data Export forExcel ============\\n# Create detailed results table\\nresult_data = {\\n \'Plant\': [],\\n \'WarehouseX\': [],\\n \'WarehouseY\': [],\\n \'WarehouseZ\': [],\\n \'Total Production\': [],\\n \'Production\': []\\n}\\n\\nfor i, factory in enumerate(factories):\\n result_data[\'Plant\'].append(factory)\\n result_data[\'WarehouseX\'].append(f\\"{solution[i, 0]:.2f}\\")\\n result_data[\'WarehouseY\'].append(f\\"{solution[i, 1]:.2f}\\")\\n result_data[\'WarehouseZ\'].append(f\\"{solution[i, 2]:.2f}\\")\\n result_data[\'Total Production\'].append(f\\"{solution[i].sum():.2f}\\")\\n result_data[\'Production\'].append(supply[factory])\\n\\nresult_df = pd.DataFrame(result_data)\\nresult_df.to_csv(\'/workspace/transport_detail.csv\', index=False)\\n\\nprint(\\"\\\\n\\" + \\"=\\" * 60)\\n"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'write_file',
+ id: null,
+ tool_call_id: 'toolu_bdrk_01TE2uRFPjayQeiq3p18XGr3',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ timestamp: 1762847706403,
+ session_id: 'b405c8032f5c4423',
+ task_id: 'b405c8032f5c4423_684215',
+ trace_id: '2d8844e6ef652c862028c43f222f29d5',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+];
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/agent-build.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/agent-build.test.ts
new file mode 100644
index 0000000..8c1dbcb
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/agent-build.test.ts
@@ -0,0 +1,200 @@
+import { describe, expect, it } from 'vitest';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+import { MessageChunk } from '@/sdk';
+
+const chunks: MessageChunk[] = [
+ {
+ id: '1768185803389_d4gi',
+ type: 'session_init',
+ content: 'Tell me a joke.',
+ role: 'assistant',
+ detail: {
+ session_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405',
+ title: 'Tell me a joke.',
+ },
+ timestamp: 1768185803389,
+ session_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405',
+ task_id: null,
+ trace_id: null,
+ trace_base_url: null,
+ field_name: null,
+ },
+ {
+ id: '1768185803450_ksjk',
+ type: 'node_start',
+ content: '',
+ role: 'assistant',
+ detail: {
+ node_id: 'start',
+ started_at: 1768185803450,
+ node_type: 'builtins.Start',
+ node_label: 'Start',
+ },
+ timestamp: 1768185803450,
+ session_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405',
+ task_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405_803390',
+ trace_id: 'b47b56c9575ff535c6f7b7d316313732',
+ trace_base_url: null,
+ field_name: null,
+ },
+
+ {
+ id: '1768185803451_u0v4',
+ type: 'node_end',
+ content: '',
+ role: 'assistant',
+ detail: {
+ node_id: 'start',
+ completed_at: 1768185803451,
+ node_type: 'builtins.Start',
+ node_label: 'Start',
+ },
+ timestamp: 1768185803451,
+ session_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405',
+ task_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405_803390',
+ trace_id: 'b47b56c9575ff535c6f7b7d316313732',
+ trace_base_url: null,
+ field_name: null,
+ },
+
+ {
+ id: '1768185803451_f0w4',
+ type: 'node_start',
+ content: '',
+ role: 'assistant',
+ detail: {
+ node_id: 'agent',
+ started_at: 1768185803451,
+ node_type: 'builtins.Agent',
+ node_label: 'My agent',
+ },
+ timestamp: 1768185803451,
+ session_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405',
+ task_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405_803390',
+ trace_id: 'b47b56c9575ff535c6f7b7d316313732',
+ trace_base_url: null,
+ field_name: null,
+ },
+
+ {
+ id: '1768185804001_y1c8',
+ type: 'finish_reason',
+ content:
+ "Error code: 401 - {'error': {'code': '', 'message': '[sk-nma***EEF] The token is exhausted. !token.UnlimitedQuota && token.RemainQuota = -23243 (request id: 20260112104323271352243TmOzTQbb)', 'type': 'new_api_error'}}",
+ role: 'assistant',
+ detail: {
+ reason:
+ "Error code: 401 - {'error': {'code': '', 'message': '[sk-nma***EEF] The token is exhausted. !token.UnlimitedQuota && token.RemainQuota = -23243 (request id: 20260112104323271352243TmOzTQbb)', 'type': 'new_api_error'}}",
+ status: 'failed',
+ },
+ timestamp: 1768185804001,
+ session_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405',
+ task_id: 'ab_b5b7286a-242a-49a4-9e40-7dc217dfa405_803390',
+ trace_id: null,
+ trace_base_url: null,
+ field_name: null,
+ },
+];
+
+describe('agent build - node start end', () => {
+ it('1', () => {
+ const messages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[]);
+ expect(messages).toEqual([
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...chunks[0],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+ it('2', () => {
+ const messages = transformChunksToMessages(chunks.slice(0, 2) as MessageChunk[]);
+ expect(messages).toEqual([
+ {
+ role: 'assistant',
+ trace_id: 'b47b56c9575ff535c6f7b7d316313732',
+ messages: [
+ {
+ ...chunks[0],
+ isLast: true,
+ },
+ {
+ ...chunks[1],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+ it('2', () => {
+ const messages = transformChunksToMessages(chunks.slice(0, 3) as MessageChunk[]);
+ expect(messages).toEqual([
+ {
+ role: 'assistant',
+ trace_id: 'b47b56c9575ff535c6f7b7d316313732',
+ messages: [
+ {
+ ...chunks[0],
+ isLast: true,
+ },
+ {
+ ...chunks[1],
+ ...chunks[2],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+});
+
+describe('transformChunksToMessages', () => {
+ it('1', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(0, 1), []);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 1));
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('2', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(1, 2),
+ transformChunksToMessages(chunks.slice(0, 1)),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 2));
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('3', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(2, 3),
+ transformChunksToMessages(chunks.slice(0, 2)),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 3));
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('4', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(3, 4),
+ transformChunksToMessages(chunks.slice(0, 3)),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 4));
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('5', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(4, 5),
+ transformChunksToMessages(chunks.slice(0, 4)),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 5));
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/browser-use.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/browser-use.test.ts
new file mode 100644
index 0000000..13ad651
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/browser-use.test.ts
@@ -0,0 +1,1456 @@
+import { describe, it, expect } from 'vitest';
+import { getLoadingMessage } from '../../utils';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+
+describe('transformChunksToMessages', () => {
+ it('1', () => {
+ const messages = transformChunksToMessages([
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ]);
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ // IncreaseloadingMessage
+ getLoadingMessage(),
+ ]);
+ });
+
+ it('2', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('3', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('4', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111827607_t0so',
+ role: 'inner_message',
+ type: 'config',
+ content: 'update_session',
+ detail: {
+ session_id: 'daeb61e93f7d4326',
+ sandbox_id: 'i1hvw3ir6vka66ru4ztu0-ecc9fd8d',
+ sandbox_url: '',
+ },
+ timestamp: 1758111827661,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('5', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111827825_1b8b',
+ role: 'assistant',
+ type: 'tool_call',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'pending',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ },
+ timestamp: 1758111827942,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111827825_1b8b',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'pending',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ },
+ timestamp: 1758111827942,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('6', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111833950_1a98',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ result: {
+ url: '',
+ title: '',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_07.png',
+ final_result: '',
+ timestamp_ns: 1758111833950838300,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ status: 'success',
+ },
+ timestamp: 1758111833961,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111827825_1b8b',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'pending',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ },
+ timestamp: 1758111827942,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111833950_1a98',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ result: {
+ url: '',
+ title: '',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_07.png',
+ final_result: '',
+ timestamp_ns: 1758111833950838300,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ status: 'success',
+ },
+ timestamp: 1758111833961,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('7', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111833974_8doh',
+ role: 'assistant',
+ type: 'tool_call',
+ content: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'pending',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ timestamp_ns: 1758111833950847500,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ },
+ timestamp: 1758111833984,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111833950_1a98',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'success',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ result: {
+ url: '',
+ title: '',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_07.png',
+ final_result: '',
+ timestamp_ns: 1758111833950838300,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ },
+ timestamp: 1758111827942,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111833950_1a98',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'success',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ result: {
+ url: '',
+ title: '',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_07.png',
+ final_result: '',
+ timestamp_ns: 1758111833950838300,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ },
+ timestamp: 1758111827942,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111833974_8doh',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'pending',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ timestamp_ns: 1758111833950847500,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ },
+ timestamp: 1758111833984,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('8', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1758111849971_az9o',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ result: {
+ url: 'https://www.baidu.com/',
+ title: 'You know it.',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_08.png',
+ final_result: '',
+ timestamp_ns: 1758111849971686000,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ status: 'success',
+ },
+ timestamp: 1758111849984,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111833950_1a98',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'success',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ result: {
+ url: '',
+ title: '',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_07.png',
+ final_result: '',
+ timestamp_ns: 1758111833950838300,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ },
+ timestamp: 1758111827942,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111833974_8doh',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'pending',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ timestamp_ns: 1758111833950847500,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ },
+ timestamp: 1758111833984,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1758111821217_b6ebc8f6',
+ role: 'user',
+ type: 'text',
+ content: 'Again.planTool planning, usebrowser-use Ask Beijing weather. Help me plan the commuting this week.\n\n',
+ detail: {},
+ timestamp: 1758111821217,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1758111825367_hao1',
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Usebrowser-useI\'m going to check for weather information for Beijing this week.\n2. Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1758111825386,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ id: '1758111825367_zpxp',
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Increasechildren
+ children: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [
+ {
+ id: '1758111833950_1a98',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ detail: {
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ status: 'success',
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Open 100 degrees. Search.“This week\'s weather forecast in Beijing.”,Get the future7Weather information, including temperature, rainfall, wind, etc.',
+ timestamp_ns: 1758111827825830400,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ result: {
+ url: '',
+ title: '',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_07.png',
+ final_result: '',
+ timestamp_ns: 1758111833950838300,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ },
+ timestamp: 1758111827942,
+ // IncreaseisLast
+ isLast: true,
+ },
+ {
+ // Replace it with the tool_result ID.
+ id: '1758111849971_az9o',
+ role: 'assistant',
+ // type Replace with tool Name
+ type: 'browser-use',
+ content: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ detail: {
+ // Reservationstool_call, and then click the
+ param: {
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ brief: 'Opens the first page in a new tab (in thousands of degrees)https://www.baidu.com)。',
+ timestamp_ns: 1758111833950847500,
+ },
+ action: 'Calling browser-use',
+ action_content: '',
+ // Usetool_result, and then click the
+ run_id: '9820dade-2897-46d6-9dfe-d51accbed76a',
+ tool: 'browser-use',
+ result: {
+ url: 'https://www.baidu.com/',
+ title: 'You know it.',
+ image_url:
+ 'https://boe.example.invalid/resources/RESOURCE_EXAMPLE_08.png',
+ final_result: '',
+ timestamp_ns: 1758111849971686000,
+ sandbox_url:
+ 'https://sandbox.example.invalid/images/SCREENSHOT_EXAMPLE_01.png',
+ },
+ status: 'success',
+ },
+ timestamp: 1758111849984,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ // Increasechildren
+ children: [],
+ },
+ ],
+
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Usebrowser-useI\'m going to check for weather information for Beijing this week.',
+ status: 'running',
+ started_at: 1758111825367,
+ },
+ {
+ id: '2',
+ title:
+ 'Planning for users of the best daily commuting method for the week, based on weather forecasts, in combination with common commuting (e.g., walking, riding, subway, bus, taxi, etc.), with an explanation of the reasons',
+ status: 'pending',
+ started_at: 1758111825367,
+ },
+ ],
+ },
+ timestamp: 1758111825422,
+ // IncreaseisLast
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/callCallEnd.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/callCallEnd.test.ts
new file mode 100644
index 0000000..a173265
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/callCallEnd.test.ts
@@ -0,0 +1,981 @@
+import { describe, it, expect } from 'vitest';
+import { getLoadingMessage } from '../../utils';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+import { MessageChunk, MessageToolChunk } from '@/types';
+
+const chunks = [
+ {
+ id: 72870,
+ role: 'user',
+ type: 'text',
+ content:
+ 'Explain the intelligence,Agent、Large models,LLM、MCPI\'m not sure I\'m going to be able to do that.Agent、Large models,LLM、MCPEach one is described in one page, and ultimately a summary is made in one page.',
+ detail: {},
+ step_id: null,
+ timestamp: 1760421811,
+ is_llm_message: false,
+ session_id: '6e9a138f04bc4ba0',
+ },
+ {
+ id: 72891,
+ role: 'assistant',
+ type: 'text',
+ content: 'Now I\'m starting to createPPT,Total9Pages, smarts,Agent、Large models,LLM、MCP、Summary page, thanks page.',
+ detail: {
+ run_id: 'baf4ad73-962c-4e9b-8694-a4cc3fa74a0d',
+ full_content: 'Now I\'m starting to createPPT,Total9Pages, smarts,Agent、Large models,LLM、MCP、Summary page, thanks page.',
+ tool_calls: [
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 1,
+ page_index: 1,
+ replace_content:
+ '{\n "text": {\n "1b32b5f1": ["AICore conceptual details: smart bodies, large models andMCPAgreements"],\n "176a219a": ["Presenter:AITechnical experts"]\n }\n}',
+ },
+ id: 'tooluse_cKTmgt4oSuG-1xTCIK0p_w',
+ type: 'tool_call',
+ },
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 2,
+ page_index: 2,
+ replace_content:
+ '{\n "text": {\n "db61c16c": ["Smart Body(s)AI Agent)"],\n "9dd5986c": ["Smart bodies are the core concept in the realm of artificial intelligence, which refers to entities that are able to feel the environment, make decisions and implement actions to achieve specific objectives. They have the characteristics of autonomy, responsiveness, sociality and adaptability, which can be software programs, robots or any system with autonomous behaviour, and are widely used in areas such as intelligent homes, autopilots, and health care."]\n }\n}',
+ },
+ id: 'tooluse_hvDYTNsmQ66g3w7dZL2dew',
+ type: 'tool_call',
+ },
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 3,
+ page_index: 3,
+ replace_content:
+ '{\n "text": {\n "a9775c2a": ["Core features of the intelligence system"],\n "722f74e7": ["Autonomy"],\n "085fd6f3": ["Reactional"],\n "1a03320d": ["Social and adaptive"],\n "e1f85f4b": ["Smart bodies operate independently, without human intervention, and can make decisions independently, based on pre-established rules or learning algorithms, to perform their tasks efficiently and without merit."],\n "bc2c8fe3": ["Smart body energy recognizes and responds to environmental change, achieving optimal results by constantly adapting its behaviour to the environment."],\n "bdff886a": ["Multiple intelligent bodies communicate, coordinate and cooperate with each other in the fulfilment of their mandates, while constantly improving their performance through learning and evolution."]\n }\n}',
+ },
+ id: 'tooluse_po0NzFweS-mMMDtLcmuikQ',
+ type: 'tool_call',
+ },
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 2,
+ page_index: 4,
+ replace_content:
+ '{\n "text": {\n "db61c16c": ["Agent(I\'m not a smart agent."],\n "9dd5986c": ["AgentIt combines external tools (search, code, code, etc.) with the core competencies of the Big Language Model (e.g., logical reasoning, knowledge base call).API(c) Accompanying, etc.) to achieve processes that are close to human-friendly and have autonomy, multi-tool collaboration and memory learning."]\n }\n}',
+ },
+ id: 'tooluse_SGCZELpdRmqRVmB6B8fuYw',
+ type: 'tool_call',
+ },
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 3,
+ page_index: 5,
+ replace_content:
+ '{\n "text": {\n "a9775c2a": ["AgentCore competencies"],\n "722f74e7": ["Autonomy planning"],\n "085fd6f3": ["Multi-tool collaboration"],\n "1a03320d": ["Memory and learning"],\n "e1f85f4b": ["You can plan steps automatically according to the user\'s target, without guidance."Help me analyze sales data.",AgentIt can call analytical tools, generate charts and summarize trends."],\n "bc2c8fe3": ["Various tools (e.g. search engine, code interpreter,APIThe interface) is completed. For example, answer"How\'s the weather?" When you call the weather first APIGet the data, and then generate the answers."],\n "bdff886a": ["AdvancedAgentRemembers user preferences and history interactions and provides personalized services. For example, keep track of user schedules and automatically alert to to-dos."]\n }\n}',
+ },
+ id: 'tooluse_Rwg6Lye6RDCNZSqh9kF_zQ',
+ type: 'tool_call',
+ },
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 2,
+ page_index: 6,
+ replace_content:
+ '{\n "text": {\n "db61c16c": ["Large model (Large Model)"],\n "9dd5986c": ["Large models are deep learning models with a large number of parameters (billions to hundreds of billions), usually based onTransformerStructure. These models are an important basis for the development of artificial intelligence technology by learning complex patterns and relationships from a large amount of data, not just in terms of language processing, but also in a variety of tasks, such as image recognition, voice processing and so forth."]\n }\n}',
+ },
+ id: 'tooluse_YwRUIa2qSvCHLgDpyUGxBw',
+ type: 'tool_call',
+ },
+ {
+ name: 'make_new_slide_from_template',
+ args: {
+ template_page_index: 3,
+ page_index: 7,
+ replace_content:
+ '{\n "text": {\n "a9775c2a": ["Core features of the large model"],\n "722f74e7": ["Size of parameters"],\n "085fd6f3": ["Pre-training and fine-tuning"],\n "1a03320d": ["Powerful panorama."],\n "e1f85f4b": ["Large models typically contain billions or even hundreds of billions of parameters, and large parameters sizes allow models to learn more complex features and patterns and to perform well in a variety of tasks."],\n "bc2c8fe3": ["The strategy of pre-training and fine-tuning is to first train without supervision on large-scale data sets, followed by supervised fine-tuning on specific tasks and effectively enhance the capacity for broad-based adaptation."],\n "bdff886a": ["Large models can be learned through migration and the knowledge learned from one mission will be applied to other related tasks, significantly reducing the need for labeled data and improving the performance of new missions."]\n }\n}',
+ },
+ id: 'tooluse_JpSoFFLSTGOizhfjPFfRyQ',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 47797,
+ output_tokens: 2001,
+ total_tokens: 49798,
+ input_token_details: { cache_creation: 0, cache_read: 0 },
+ },
+ },
+ step_id: '',
+ timestamp: 1760421911,
+ is_llm_message: false,
+ session_id: '6e9a138f04bc4ba0',
+ },
+ {
+ id: 72892,
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '5f1ed0eb-cedc-4cff-970a-b07636c20d7e',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 1,
+ page_index: 1,
+ replace_content:
+ '{\n "text": {\n "1b32b5f1": ["AICore conceptual details: smart bodies, large models andMCPAgreements"],\n "176a219a": ["Presenter:AITechnical experts"]\n }\n}',
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ step_id: '',
+ timestamp: 1760421920,
+ is_llm_message: false,
+ session_id: '6e9a138f04bc4ba0',
+ },
+ {
+ id: 72893,
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '5c036d3e-22d2-4f82-85d3-4cdb7389a728',
+ tool: 'make_new_slide_from_template',
+ status: 'pending',
+ param: {
+ template_page_index: 2,
+ page_index: 2,
+ replace_content:
+ '{\n "text": {\n "db61c16c": ["Smart Body(s)AI Agent)"],\n "9dd5986c": ["Smart bodies are the core concept in the realm of artificial intelligence, which refers to entities that are able to feel the environment, make decisions and implement actions to achieve specific objectives. They have the characteristics of autonomy, responsiveness, sociality and adaptability, which can be software programs, robots or any system with autonomous behaviour, and are widely used in areas such as intelligent homes, autopilots, and health care."]\n }\n}',
+ },
+ action: 'Calling make_new_slide_from_template',
+ action_content: '',
+ },
+ step_id: '',
+ timestamp: 1760421923,
+ is_llm_message: false,
+ session_id: '6e9a138f04bc4ba0',
+ },
+ {
+ id: 72899,
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ run_id: '5f1ed0eb-cedc-4cff-970a-b07636c20d7e',
+ tool: 'make_new_slide_from_template',
+ result: {
+ content:
+ '{"content": "✅ **I\'m sorry. 1 Page production completed**\\n📄 **Page Title**: AICore conceptual details: smart bodies, large models andMCPAgreements\\n📝 **Page Content Outline**:\\n 1. AICore conceptual details: smart bodies, large models andMCPAgreements\\n 2. Presenter:AITechnical experts\\n📊 **Statistics**: 2 Text elements\\n💡 Template-based page design, content successfully filled and saved", "content_type": "markdown", "page_index": 1, "template_page_index": 1, "status": "success"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'make_new_slide_from_template',
+ id: 'a5bf8b50-5c09-4541-a641-02e8bd8590ef',
+ tool_call_id: 'tooluse_cKTmgt4oSuG-1xTCIK0p_w',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ },
+ step_id: '',
+ timestamp: 1760421923,
+ is_llm_message: false,
+ session_id: '6e9a138f04bc4ba0',
+ },
+];
+
+const plan = {
+ id: 72888,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ },
+ { id: '2', title: 'Select the rightPPTTemplates and analysis of template structures', status: 'success', started_at: 1760421873495 },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ },
+ { id: '4', title: 'UploadPPTGo to Sandbox and generate download links', status: 'pending', started_at: 1760421873495 },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495 },
+ ],
+ },
+ step_id: '',
+ timestamp: 1760421873,
+ is_llm_message: false,
+ session_id: '6e9a138f04bc4ba0',
+};
+
+describe('multiple tool_call then multiple tool_end', () => {
+ it('1', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[], []);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0]],
+ },
+ getLoadingMessage(),
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('2', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(1, 2) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ getLoadingMessage(),
+ ]);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 2) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ ],
+ },
+ ];
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('3', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(2, 3) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 3) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ ];
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('4', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(3, 4) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 4) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(3, 4)[0] as MessageChunk),
+ type: (chunks.slice(3, 4)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('5', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(4, 5) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(3, 4)[0] as MessageChunk),
+ type: (chunks.slice(3, 4)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 5) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks.slice(0, 1)[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(chunks.slice(1, 2)[0] as MessageChunk),
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(4, 5)[0] as MessageChunk),
+ type: (chunks.slice(4, 5)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ detail: {
+ ...(chunks.slice(4, 5)[0] as MessageToolChunk).detail,
+ param: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.param,
+ action: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.action,
+ action_content: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.action_content,
+ },
+ },
+ {
+ ...(chunks.slice(3, 4)[0] as MessageChunk),
+ type: (chunks.slice(3, 4)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+});
+
+describe('multiple tool_call then multiple tool_end in plan', () => {
+ it('User Input', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[], []);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0]],
+ },
+ getLoadingMessage(),
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('AIReplyplan', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ [plan as MessageChunk],
+ [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ getLoadingMessage(),
+ ],
+ );
+ const onceMessages = transformChunksToMessages([...(chunks.slice(0, 1) as MessageChunk[]), plan as MessageChunk]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0]],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('planIncreasetext', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(1, 2) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ const onceMessages = transformChunksToMessages([
+ ...(chunks.slice(0, 1) as MessageChunk[]),
+ plan as MessageChunk,
+ ...(chunks.slice(1, 2) as MessageChunk[]),
+ ]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [{ ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true }],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('planIncreasetool_call1', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(2, 3) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [{ ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true }],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages([
+ ...(chunks.slice(0, 1) as MessageChunk[]),
+ plan as MessageChunk,
+ ...(chunks.slice(1, 3) as MessageChunk[]),
+ ]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [
+ { ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('planIncreasetool_call2', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(3, 4) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [
+ { ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ const onceMessages = transformChunksToMessages([
+ ...(chunks.slice(0, 1) as MessageChunk[]),
+ plan as MessageChunk,
+ ...(chunks.slice(1, 4) as MessageChunk[]),
+ ]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [
+ { ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(3, 4)[0] as MessageChunk),
+ type: (chunks.slice(3, 4)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ];
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+ it('planIncreasetool_end1', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(4, 5) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [
+ { ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true },
+ {
+ ...(chunks.slice(2, 3)[0] as MessageChunk),
+ type: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(3, 4)[0] as MessageChunk),
+ type: (chunks.slice(3, 4)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ const onceMessages = transformChunksToMessages([
+ ...(chunks.slice(0, 1) as MessageChunk[]),
+ plan as MessageChunk,
+ ...(chunks.slice(1, 5) as MessageChunk[]),
+ ]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [chunks[0] as MessageChunk],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ ...(plan as MessageChunk),
+ children: [
+ {
+ id: '1',
+ title: 'Collecting intelligence,Agent、Large models,LLM、MCPand detailed conceptual information',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Select the rightPPTTemplates and analysis of template structures',
+ status: 'success',
+ started_at: 1760421873495,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'CreatePPT:Cover Page + 5Concept pages (smart,Agent、Large models,LLM、MCP)+ Summary page + Acknowledgement page, total9Page',
+ status: 'running',
+ started_at: 1760421873495,
+ children: [
+ { ...(chunks.slice(1, 2)[0] as MessageChunk), isLast: true },
+ {
+ ...(chunks.slice(4, 5)[0] as MessageChunk),
+ type: (chunks.slice(4, 5)[0] as MessageToolChunk).detail.tool,
+ detail: {
+ ...(chunks.slice(4, 5)[0] as MessageToolChunk).detail,
+ param: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.param,
+ action: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.action,
+ action_content: (chunks.slice(2, 3)[0] as MessageToolChunk).detail.action_content,
+ },
+ isLast: true,
+ },
+ {
+ ...(chunks.slice(3, 4)[0] as MessageChunk),
+ type: (chunks.slice(3, 4)[0] as MessageToolChunk).detail.tool,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '4',
+ title: 'UploadPPTGo to Sandbox and generate download links',
+ status: 'pending',
+ started_at: 1760421873495,
+ children: [],
+ },
+ { id: '5', title: 'Delivery of final file to user', status: 'pending', started_at: 1760421873495, children: [] },
+ ],
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/index.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/index.test.ts
new file mode 100644
index 0000000..d89aa34
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/index.test.ts
@@ -0,0 +1,1441 @@
+import { describe, it, expect } from 'vitest';
+import { pptChunks } from '../mock';
+import { getLoadingMessage } from '../../utils';
+import { MessageChunk } from '@/types';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+
+describe('transformChunksToMessages', () => {
+ it('1', () => {
+ const messages = transformChunksToMessages(pptChunks.slice(0, 1) as MessageChunk[]);
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [pptChunks[0] as MessageChunk],
+ },
+ getLoadingMessage(),
+ ]);
+ });
+
+ it('2', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ },
+ ] as MessageChunk[],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('3', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('4', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('5', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('6', () => {
+ const messages = transformChunksToMessages(pptChunks.slice(5, 6) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784071572_753e6987',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ tool: 'choose_and_read_template_pptx_info',
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ result: {
+ content:
+ '{"content": "[\\n {\\n \\"slide_number\\": 1,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"be179f3b\\": [\\n \\"01AI Presentation\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 2,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"83b517ce\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"682a9d2a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"64b215f0\\": [\\n \\"04\\"\\n ],\\n \\"19a37359\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"1aa366ef\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"db61c16c\\": [\\n \\"02\\"\\n ],\\n \\"9dd5986c\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"eb5d339f\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"8b642dcc\\": [\\n \\"03\\"\\n ],\\n \\"9e319fcb\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"ee8da08a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"f2a48842\\": [\\n \\"01\\"\\n ],\\n \\"8351ea2e\\": [\\n \\"Contents\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 3,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"a9775c2a\\": [\\n \\"The introduction to the 101st.\\"\\n ],\\n \\"722f74e7\\": [\\n \\"“01.AI.”• Digitalization, based on zero and one, to connect and manage everything with intelligence, and to promote digital transformation and intellectual social building.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 4,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"f9ee6954\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platform, which will effectively help to raise human productivity and thus create huge economic and social values with a strong technological vision and artificial intelligence belief, and zero things dedicated to building a whole new one AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"c15b6480\\": [\\n \\"Text Titles\\"\\n ],\\n \\"a34fe09f\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"dcfced38\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"5655a992\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"9f58fe6c\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"4a064350\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"23129021\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"96cc470d\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"309ca158\\": [\\n \\"AI 2.0 Platform\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 5,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"eae66713\\": [\\n \\"The Philosophy and Maths Meaning of 01\\"\\n ],\\n \\"14b01155\\": [\\n \\"Zero Origin and Development\\"\\n ],\\n \\"4b8d5330\\": [\\n \\"The symbolism of the first.\\"\\n ],\\n \\"e1543e6b\\": [\\n \\"Zero and one in mathematics.\\"\\n ],\\n \\"a1b45b87\\": [\\n \\"The concept of zero is rooted in ancient civilization, where Indian mathematicians use zero as an independent digital invention to promote the improvement of the mathematical system and scientific progress.\\"\\n ],\\n \\"7d8cb902\\": [\\n \\"The first symbol of unity and starting point of a number is the beginning and multiplication of the natural number, representing the unity of the whole and the source of all things.\\"\\n ],\\n \\"75ef77d4\\": [\\n \\"The zero and one are mathematical building blocks, zero are plus and minus elements of identity, and one is the multiplication core, which forms the basis of the digital system and facilitates computing and information processing.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 6,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"fc354462\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"81c521dc\\": [\\n \\"Binary system base\\"\\n ],\\n \\"c9ff1628\\": [\\n \\"How computers operate with zero and one.\\"\\n ],\\n \\"477c00b4\\": [\\n \\"Zero Encoding and Data Storage\\"\\n ],\\n \\"903bf9c9\\": [\\n \\"1\\"\\n ],\\n \\"9c00c483\\": [\\n \\"The binary system, which expresses all data and instructions in zero and one or two states, is the core language of computers and digital equipment, simplifys circuit design and ensures accurate data transmission.\\"\\n ],\\n \\"131611a9\\": [\\n \\"2\\"\\n ],\\n \\"7fe0b43b\\": [\\n \\"Computers convert information to binary codes, perform arithmetic and logical operations through the logical door, and achieve complex data processing and program execution.\\"\\n ],\\n \\"52f74783\\": [\\n \\"3\\"\\n ],\\n \\"ff45f2a8\\": [\\n \\"The 01 code converts information to binary format suitable for storage and transmission, and common code is availableASCIIandUnicode,Storage devices preserve data through charge change.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 7,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"c5d8ade1\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"b0993f80\\": [\\n \\"The way things are digitally realized.\\"\\n ],\\n \\"5e3209a4\\": [\\n \\"The seamless connection of entities to the digital world is achieved through sensor collection of data, real-time network transmission, cloud computing storage analysis and smart algorithm decision-making.\\"\\n ],\\n \\"27103ac8\\": [\\n \\"101 applications in the Internet\\"\\n ],\\n \\"71712164\\": [\\n \\"The 01 Code serves as the basis for binary development, ensuring the accurate transmission of data and efficient collaboration of equipment in the Internet of goods, and promoting intelligent home and intelligent urban development.\\"\\n ],\\n \\"de614814\\": [\\n \\"Digital twinning technology profile\\"\\n ],\\n \\"31b55279\\": [\\n \\"The integration of the digital economy is facilitated by the use of zero data combined with three-dimensional modelling to optimize real-time mapping, failure prediction and maintenance of physical equipment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 8,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"dff43bdd\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"75d19ee0\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"c4a426df\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"00e037d3\\": [\\n \\"Zero One Technology for Industrial Change\\"\\n ],\\n \\"b4de8dbb\\": [\\n \\"The zero-one technology promotes manufacturing intelligence, precision management in agriculture and digitalization of services, and the re-establishment of industrial chains and high-quality economic development.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 9,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"4b7576d4\\": [\\n \\"The challenges and problems of 01.AI.\\"\\n ],\\n \\"a1ca8c3f\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"f9176f86\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"fbec61e5\\": [\\n \\"Technological divide and digital exclusion\\"\\n ],\\n \\"e77ae3d9\\": [\\n \\"The technological divide, which leads to the digital exclusion of some groups, requires the reduction of the urban-rural gap, the upgrading of digital literacy and the expansion of infrastructure to achieve inclusive development in a digital society.\\"\\n ],\\n \\"b9b4bb28\\": [\\n \\"Ethical and legal issues\\"\\n ],\\n \\"67598b22\\": [\\n \\"Development of zero-sum objects presents ethical and legal challenges, requiring the establishment of regulatory and legal frameworks that guarantee the legitimate use of data, the transparency of artificial intelligence and a rational response to the effects of employment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 10,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"5144e569\\": [\\n \\"The future of 01.AI.\\"\\n ],\\n \\"db5ff530\\": [\\n \\"Integration of artificial intelligence with the one technology\\"\\n ],\\n \\"2e7e6eec\\": [\\n \\"The zero one technology provides the basic data expression and computing of artificial intelligence and promotes the upgrading of the intelligence system through the chain.\\"\\n ],\\n \\"7f245244\\": [\\n \\"Impact of quantum calculations on the 101 system\\"\\n ],\\n \\"bdbde1cb\\": [\\n \\"Quantum computing has broken the traditional 01 limit, promoting the development of classic Zero-one technologies and quantum computing in combination, and expanding the boundaries of computing capabilities.\\"\\n ],\\n \\"0a629dfc\\": [\\n \\"Smart cities and the construction of intelligent societies\\"\\n ],\\n \\"e2197e6e\\": [\\n \\"Zero-one technology drives the smart city core system to achieve intelligent management and improve quality of life and the efficiency of urban operations.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 11,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"61d550f6\\": [\\n \\"Summary and reflection\\"\\n ],\\n \\"86dba575\\": [\\n \\"A review of the core values of 01.AI.\\"\\n ],\\n \\"f4abbd51\\": [\\n \\"The zero-sum potential of the basic unit of information is reflected in the underlying concepts of the digital age, driving the digital transformation and industrial upgrading.\\"\\n ],\\n \\"4428e62e\\": [\\n \\"Digital thinking for the future\\"\\n ],\\n \\"cd06c39d\\": [\\n \\"The integration of emerging technologies has driven the expansion of the application of 101 things, and digital transformations have transformed productive life, with attention to equity and data security.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 12,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"33017331\\": [\\n \\"Thank you.\\"\\n ]\\n }\\n }\\n }\\n]", "content_type": "markdown", "status": "success", "template_path": "/tmp/doc/uploaded_template_109f091d397042ed_01.AI.-Light.pptx"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'choose_and_read_template_pptx_info',
+ id: '47c60773-a8ec-42a4-966f-7fc775888fcd',
+ tool_call_id: 'tooluse_Zk6nBNkWQSSo41jwhYZKPQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071578,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('7', () => {
+ const messages = transformChunksToMessages(pptChunks.slice(6, 7) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ tool: 'choose_and_read_template_pptx_info',
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ result: {
+ content:
+ '{"content": "[\\n {\\n \\"slide_number\\": 1,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"be179f3b\\": [\\n \\"01AI Presentation\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 2,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"83b517ce\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"682a9d2a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"64b215f0\\": [\\n \\"04\\"\\n ],\\n \\"19a37359\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"1aa366ef\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"db61c16c\\": [\\n \\"02\\"\\n ],\\n \\"9dd5986c\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"eb5d339f\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"8b642dcc\\": [\\n \\"03\\"\\n ],\\n \\"9e319fcb\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"ee8da08a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"f2a48842\\": [\\n \\"01\\"\\n ],\\n \\"8351ea2e\\": [\\n \\"Contents\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 3,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"a9775c2a\\": [\\n \\"The introduction to the 101st.\\"\\n ],\\n \\"722f74e7\\": [\\n \\"“01.AI.”• Digitalization, based on zero and one, to connect and manage everything with intelligence, and to promote digital transformation and intellectual social building.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 4,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"f9ee6954\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platform, which will effectively help to raise human productivity and thus create huge economic and social values with a strong technological vision and artificial intelligence belief, and zero things dedicated to building a whole new one AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"c15b6480\\": [\\n \\"Text Titles\\"\\n ],\\n \\"a34fe09f\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"dcfced38\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"5655a992\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"9f58fe6c\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"4a064350\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"23129021\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"96cc470d\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"309ca158\\": [\\n \\"AI 2.0 Platform\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 5,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"eae66713\\": [\\n \\"The Philosophy and Maths Meaning of 01\\"\\n ],\\n \\"14b01155\\": [\\n \\"Zero Origin and Development\\"\\n ],\\n \\"4b8d5330\\": [\\n \\"The symbolism of the first.\\"\\n ],\\n \\"e1543e6b\\": [\\n \\"Zero and one in mathematics.\\"\\n ],\\n \\"a1b45b87\\": [\\n \\"The concept of zero is rooted in ancient civilization, where Indian mathematicians use zero as an independent digital invention to promote the improvement of the mathematical system and scientific progress.\\"\\n ],\\n \\"7d8cb902\\": [\\n \\"The first symbol of unity and starting point of a number is the beginning and multiplication of the natural number, representing the unity of the whole and the source of all things.\\"\\n ],\\n \\"75ef77d4\\": [\\n \\"The zero and one are mathematical building blocks, zero are plus and minus elements of identity, and one is the multiplication core, which forms the basis of the digital system and facilitates computing and information processing.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 6,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"fc354462\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"81c521dc\\": [\\n \\"Binary system base\\"\\n ],\\n \\"c9ff1628\\": [\\n \\"How computers operate with zero and one.\\"\\n ],\\n \\"477c00b4\\": [\\n \\"Zero Encoding and Data Storage\\"\\n ],\\n \\"903bf9c9\\": [\\n \\"1\\"\\n ],\\n \\"9c00c483\\": [\\n \\"The binary system, which expresses all data and instructions in zero and one or two states, is the core language of computers and digital equipment, simplifys circuit design and ensures accurate data transmission.\\"\\n ],\\n \\"131611a9\\": [\\n \\"2\\"\\n ],\\n \\"7fe0b43b\\": [\\n \\"Computers convert information to binary codes, perform arithmetic and logical operations through the logical door, and achieve complex data processing and program execution.\\"\\n ],\\n \\"52f74783\\": [\\n \\"3\\"\\n ],\\n \\"ff45f2a8\\": [\\n \\"The 01 code converts information to binary format suitable for storage and transmission, and common code is availableASCIIandUnicode,Storage devices preserve data through charge change.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 7,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"c5d8ade1\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"b0993f80\\": [\\n \\"The way things are digitally realized.\\"\\n ],\\n \\"5e3209a4\\": [\\n \\"The seamless connection of entities to the digital world is achieved through sensor collection of data, real-time network transmission, cloud computing storage analysis and smart algorithm decision-making.\\"\\n ],\\n \\"27103ac8\\": [\\n \\"101 applications in the Internet\\"\\n ],\\n \\"71712164\\": [\\n \\"The 01 Code serves as the basis for binary development, ensuring the accurate transmission of data and efficient collaboration of equipment in the Internet of goods, and promoting intelligent home and intelligent urban development.\\"\\n ],\\n \\"de614814\\": [\\n \\"Digital twinning technology profile\\"\\n ],\\n \\"31b55279\\": [\\n \\"The integration of the digital economy is facilitated by the use of zero data combined with three-dimensional modelling to optimize real-time mapping, failure prediction and maintenance of physical equipment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 8,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"dff43bdd\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"75d19ee0\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"c4a426df\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"00e037d3\\": [\\n \\"Zero One Technology for Industrial Change\\"\\n ],\\n \\"b4de8dbb\\": [\\n \\"The zero-one technology promotes manufacturing intelligence, precision management in agriculture and digitalization of services, and the re-establishment of industrial chains and high-quality economic development.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 9,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"4b7576d4\\": [\\n \\"The challenges and problems of 01.AI.\\"\\n ],\\n \\"a1ca8c3f\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"f9176f86\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"fbec61e5\\": [\\n \\"Technological divide and digital exclusion\\"\\n ],\\n \\"e77ae3d9\\": [\\n \\"The technological divide, which leads to the digital exclusion of some groups, requires the reduction of the urban-rural gap, the upgrading of digital literacy and the expansion of infrastructure to achieve inclusive development in a digital society.\\"\\n ],\\n \\"b9b4bb28\\": [\\n \\"Ethical and legal issues\\"\\n ],\\n \\"67598b22\\": [\\n \\"Development of zero-sum objects presents ethical and legal challenges, requiring the establishment of regulatory and legal frameworks that guarantee the legitimate use of data, the transparency of artificial intelligence and a rational response to the effects of employment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 10,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"5144e569\\": [\\n \\"The future of 01.AI.\\"\\n ],\\n \\"db5ff530\\": [\\n \\"Integration of artificial intelligence with the one technology\\"\\n ],\\n \\"2e7e6eec\\": [\\n \\"The zero one technology provides the basic data expression and computing of artificial intelligence and promotes the upgrading of the intelligence system through the chain.\\"\\n ],\\n \\"7f245244\\": [\\n \\"Impact of quantum calculations on the 101 system\\"\\n ],\\n \\"bdbde1cb\\": [\\n \\"Quantum computing has broken the traditional 01 limit, promoting the development of classic Zero-one technologies and quantum computing in combination, and expanding the boundaries of computing capabilities.\\"\\n ],\\n \\"0a629dfc\\": [\\n \\"Smart cities and the construction of intelligent societies\\"\\n ],\\n \\"e2197e6e\\": [\\n \\"Zero-one technology drives the smart city core system to achieve intelligent management and improve quality of life and the efficiency of urban operations.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 11,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"61d550f6\\": [\\n \\"Summary and reflection\\"\\n ],\\n \\"86dba575\\": [\\n \\"A review of the core values of 01.AI.\\"\\n ],\\n \\"f4abbd51\\": [\\n \\"The zero-sum potential of the basic unit of information is reflected in the underlying concepts of the digital age, driving the digital transformation and industrial upgrading.\\"\\n ],\\n \\"4428e62e\\": [\\n \\"Digital thinking for the future\\"\\n ],\\n \\"cd06c39d\\": [\\n \\"The integration of emerging technologies has driven the expansion of the application of 101 things, and digital transformations have transformed productive life, with attention to equity and data security.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 12,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"33017331\\": [\\n \\"Thank you.\\"\\n ]\\n }\\n }\\n }\\n]", "content_type": "markdown", "status": "success", "template_path": "/tmp/doc/uploaded_template_109f091d397042ed_01.AI.-Light.pptx"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'choose_and_read_template_pptx_info',
+ id: '47c60773-a8ec-42a4-966f-7fc775888fcd',
+ tool_call_id: 'tooluse_Zk6nBNkWQSSo41jwhYZKPQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'success',
+ started_at: 1756784075645,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ tool: 'choose_and_read_template_pptx_info',
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ result: {
+ content:
+ '{"content": "[\\n {\\n \\"slide_number\\": 1,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"be179f3b\\": [\\n \\"01AI Presentation\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 2,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"83b517ce\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"682a9d2a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"64b215f0\\": [\\n \\"04\\"\\n ],\\n \\"19a37359\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"1aa366ef\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"db61c16c\\": [\\n \\"02\\"\\n ],\\n \\"9dd5986c\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"eb5d339f\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"8b642dcc\\": [\\n \\"03\\"\\n ],\\n \\"9e319fcb\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"ee8da08a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"f2a48842\\": [\\n \\"01\\"\\n ],\\n \\"8351ea2e\\": [\\n \\"Contents\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 3,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"a9775c2a\\": [\\n \\"The introduction to the 101st.\\"\\n ],\\n \\"722f74e7\\": [\\n \\"“01.AI.”• Digitalization, based on zero and one, to connect and manage everything with intelligence, and to promote digital transformation and intellectual social building.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 4,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"f9ee6954\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platform, which will effectively help to raise human productivity and thus create huge economic and social values with a strong technological vision and artificial intelligence belief, and zero things dedicated to building a whole new one AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"c15b6480\\": [\\n \\"Text Titles\\"\\n ],\\n \\"a34fe09f\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"dcfced38\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"5655a992\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"9f58fe6c\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"4a064350\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"23129021\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"96cc470d\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"309ca158\\": [\\n \\"AI 2.0 Platform\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 5,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"eae66713\\": [\\n \\"The Philosophy and Maths Meaning of 01\\"\\n ],\\n \\"14b01155\\": [\\n \\"Zero Origin and Development\\"\\n ],\\n \\"4b8d5330\\": [\\n \\"The symbolism of the first.\\"\\n ],\\n \\"e1543e6b\\": [\\n \\"Zero and one in mathematics.\\"\\n ],\\n \\"a1b45b87\\": [\\n \\"The concept of zero is rooted in ancient civilization, where Indian mathematicians use zero as an independent digital invention to promote the improvement of the mathematical system and scientific progress.\\"\\n ],\\n \\"7d8cb902\\": [\\n \\"The first symbol of unity and starting point of a number is the beginning and multiplication of the natural number, representing the unity of the whole and the source of all things.\\"\\n ],\\n \\"75ef77d4\\": [\\n \\"The zero and one are mathematical building blocks, zero are plus and minus elements of identity, and one is the multiplication core, which forms the basis of the digital system and facilitates computing and information processing.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 6,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"fc354462\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"81c521dc\\": [\\n \\"Binary system base\\"\\n ],\\n \\"c9ff1628\\": [\\n \\"How computers operate with zero and one.\\"\\n ],\\n \\"477c00b4\\": [\\n \\"Zero Encoding and Data Storage\\"\\n ],\\n \\"903bf9c9\\": [\\n \\"1\\"\\n ],\\n \\"9c00c483\\": [\\n \\"The binary system, which expresses all data and instructions in zero and one or two states, is the core language of computers and digital equipment, simplifys circuit design and ensures accurate data transmission.\\"\\n ],\\n \\"131611a9\\": [\\n \\"2\\"\\n ],\\n \\"7fe0b43b\\": [\\n \\"Computers convert information to binary codes, perform arithmetic and logical operations through the logical door, and achieve complex data processing and program execution.\\"\\n ],\\n \\"52f74783\\": [\\n \\"3\\"\\n ],\\n \\"ff45f2a8\\": [\\n \\"The 01 code converts information to binary format suitable for storage and transmission, and common code is availableASCIIandUnicode,Storage devices preserve data through charge change.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 7,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"c5d8ade1\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"b0993f80\\": [\\n \\"The way things are digitally realized.\\"\\n ],\\n \\"5e3209a4\\": [\\n \\"The seamless connection of entities to the digital world is achieved through sensor collection of data, real-time network transmission, cloud computing storage analysis and smart algorithm decision-making.\\"\\n ],\\n \\"27103ac8\\": [\\n \\"101 applications in the Internet\\"\\n ],\\n \\"71712164\\": [\\n \\"The 01 Code serves as the basis for binary development, ensuring the accurate transmission of data and efficient collaboration of equipment in the Internet of goods, and promoting intelligent home and intelligent urban development.\\"\\n ],\\n \\"de614814\\": [\\n \\"Digital twinning technology profile\\"\\n ],\\n \\"31b55279\\": [\\n \\"The integration of the digital economy is facilitated by the use of zero data combined with three-dimensional modelling to optimize real-time mapping, failure prediction and maintenance of physical equipment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 8,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"dff43bdd\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"75d19ee0\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"c4a426df\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"00e037d3\\": [\\n \\"Zero One Technology for Industrial Change\\"\\n ],\\n \\"b4de8dbb\\": [\\n \\"The zero-one technology promotes manufacturing intelligence, precision management in agriculture and digitalization of services, and the re-establishment of industrial chains and high-quality economic development.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 9,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"4b7576d4\\": [\\n \\"The challenges and problems of 01.AI.\\"\\n ],\\n \\"a1ca8c3f\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"f9176f86\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"fbec61e5\\": [\\n \\"Technological divide and digital exclusion\\"\\n ],\\n \\"e77ae3d9\\": [\\n \\"The technological divide, which leads to the digital exclusion of some groups, requires the reduction of the urban-rural gap, the upgrading of digital literacy and the expansion of infrastructure to achieve inclusive development in a digital society.\\"\\n ],\\n \\"b9b4bb28\\": [\\n \\"Ethical and legal issues\\"\\n ],\\n \\"67598b22\\": [\\n \\"Development of zero-sum objects presents ethical and legal challenges, requiring the establishment of regulatory and legal frameworks that guarantee the legitimate use of data, the transparency of artificial intelligence and a rational response to the effects of employment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 10,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"5144e569\\": [\\n \\"The future of 01.AI.\\"\\n ],\\n \\"db5ff530\\": [\\n \\"Integration of artificial intelligence with the one technology\\"\\n ],\\n \\"2e7e6eec\\": [\\n \\"The zero one technology provides the basic data expression and computing of artificial intelligence and promotes the upgrading of the intelligence system through the chain.\\"\\n ],\\n \\"7f245244\\": [\\n \\"Impact of quantum calculations on the 101 system\\"\\n ],\\n \\"bdbde1cb\\": [\\n \\"Quantum computing has broken the traditional 01 limit, promoting the development of classic Zero-one technologies and quantum computing in combination, and expanding the boundaries of computing capabilities.\\"\\n ],\\n \\"0a629dfc\\": [\\n \\"Smart cities and the construction of intelligent societies\\"\\n ],\\n \\"e2197e6e\\": [\\n \\"Zero-one technology drives the smart city core system to achieve intelligent management and improve quality of life and the efficiency of urban operations.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 11,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"61d550f6\\": [\\n \\"Summary and reflection\\"\\n ],\\n \\"86dba575\\": [\\n \\"A review of the core values of 01.AI.\\"\\n ],\\n \\"f4abbd51\\": [\\n \\"The zero-sum potential of the basic unit of information is reflected in the underlying concepts of the digital age, driving the digital transformation and industrial upgrading.\\"\\n ],\\n \\"4428e62e\\": [\\n \\"Digital thinking for the future\\"\\n ],\\n \\"cd06c39d\\": [\\n \\"The integration of emerging technologies has driven the expansion of the application of 101 things, and digital transformations have transformed productive life, with attention to equity and data security.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 12,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"33017331\\": [\\n \\"Thank you.\\"\\n ]\\n }\\n }\\n }\\n]", "content_type": "markdown", "status": "success", "template_path": "/tmp/doc/uploaded_template_109f091d397042ed_01.AI.-Light.pptx"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'choose_and_read_template_pptx_info',
+ id: '47c60773-a8ec-42a4-966f-7fc775888fcd',
+ tool_call_id: 'tooluse_Zk6nBNkWQSSo41jwhYZKPQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'running',
+ started_at: 1756784075645,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/moveFiles.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/moveFiles.test.ts
new file mode 100644
index 0000000..6503cf5
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/moveFiles.test.ts
@@ -0,0 +1,2382 @@
+import { describe, it, expect } from 'vitest';
+import { getLoadingMessage } from '../../utils';
+import { MessageChunk } from '@/types';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+
+describe('transformChunksToMessages', () => {
+ it('moveFiles 1', () => {
+ const messages = transformChunksToMessages([
+ {
+ id: 222523,
+ role: 'user',
+ type: 'text',
+ content: 'Write a 10,000-word report analyzing the causes of nationwide housing-price fluctuations. Include charts in the report and deliver it as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1758628384,
+ is_llm_message: false,
+ session_id: 'fc7ab6bb39d14b08',
+ },
+ ]);
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 222523,
+ role: 'user',
+ type: 'text',
+ content: 'Write a 10,000-word report analyzing the causes of nationwide housing-price fluctuations. Include charts in the report and deliver it as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1758628384,
+ is_llm_message: false,
+ session_id: 'fc7ab6bb39d14b08',
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ]);
+ });
+
+ it('moveFiles 2', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: 222524,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Gather data and sources about nationwide housing-price fluctuations\n2. Analyze the main causes and contributing factors\n3. Create charts and data visualizations\n4. Write a 10,000-word analysis report\n5. Convert the report to PDF\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1758628390,
+ is_llm_message: false,
+ session_id: 'fc7ab6bb39d14b08',
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 222523,
+ role: 'user',
+ type: 'text',
+ content: 'Write a 10,000-word report analyzing the causes of nationwide housing-price fluctuations. Include charts in the report and deliver it as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1758628384,
+ is_llm_message: false,
+ session_id: 'fc7ab6bb39d14b08',
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ],
+ );
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 222523,
+ role: 'user',
+ type: 'text',
+ content: 'Write a 10,000-word report analyzing the causes of nationwide housing-price fluctuations. Include charts in the report and deliver it as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1758628384,
+ is_llm_message: false,
+ session_id: 'fc7ab6bb39d14b08',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 222524,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Gather data and sources about nationwide housing-price fluctuations\n2. Analyze the main causes and contributing factors\n3. Create charts and data visualizations\n4. Write a 10,000-word analysis report\n5. Convert the report to PDF\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1758628390,
+ is_llm_message: false,
+ session_id: 'fc7ab6bb39d14b08',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+});
+
+const chunks2 = [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ {
+ id: 461,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ detail: {
+ run_id: '05f254a4-e8e3-475b-9b29-39c56bd1850e',
+ full_content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ tool_calls: [
+ {
+ name: 'web_search',
+ args: {
+ query: '2024-2025 National Housing-Price Outlook and Real Estate Market Analysis',
+ query_num: 15,
+ },
+ id: 'call_dcqb8h8cgwdxts36ig37hid0',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 5178,
+ output_tokens: 42,
+ total_tokens: 5220,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045823,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ {
+ id: 538,
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: 'a3a48e81-3949-421a-a2e9-244dd3d335b6',
+ tool: 'agent_result_delivery',
+ status: 'pending',
+ param: {
+ attachments: [
+ '/workspace/national-housing-price-outlook-report.md',
+ '/workspace/housing-price-trend-1.png',
+ '/workspace/year-over-year-housing-price-change.png',
+ '/workspace/housing-price-comparison-by-city-tier.png',
+ ],
+ },
+ action: 'Calling agent_result_delivery',
+ action_content: '',
+ },
+ step_id: '',
+ timestamp: 1759046286,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ {
+ id: 539,
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ detail: {
+ run_id: 'a3a48e81-3949-421a-a2e9-244dd3d335b6',
+ tool: 'agent_result_delivery',
+ result: {
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'agent_result_delivery',
+ id: null,
+ tool_call_id: 'call_uaw9w2zkncg5w67twzmi5lgs',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ attachments: [
+ {
+ filename: 'todo.md',
+ path: '',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/todo.md',
+ size: 244,
+ content_type: 'application/octet-stream',
+ show_user: 0,
+ },
+ {
+ filename: 'housing-price-trend-1.png',
+ path: '/workspace/housing-price-trend-1.png',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/housing-price-trend-1.png',
+ size: 283361,
+ content_type: 'image/png',
+ show_user: 1,
+ },
+ {
+ filename: 'year-over-year-housing-price-change.png',
+ path: '/workspace/year-over-year-housing-price-change.png',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/year-over-year-housing-price-change.png',
+ size: 184938,
+ content_type: 'image/png',
+ show_user: 1,
+ },
+ {
+ filename: 'housing-price-comparison-by-city-tier.png',
+ path: '/workspace/housing-price-comparison-by-city-tier.png',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/housing-price-comparison-by-city-tier.png',
+ size: 124350,
+ content_type: 'image/png',
+ show_user: 1,
+ },
+ {
+ filename: 'national-housing-price-outlook-report.md',
+ path: '/workspace/national-housing-price-outlook-report.md',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/national-housing-price-outlook-report.md',
+ size: 7616,
+ content_type: 'application/octet-stream',
+ show_user: 1,
+ },
+ {
+ filename: 'report_style.css',
+ path: '',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/report_style.css',
+ size: 2005,
+ content_type: 'text/css',
+ show_user: 0,
+ },
+ {
+ filename: 'national-housing-price-outlook-report.html',
+ path: '',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/national-housing-price-outlook-report.html',
+ size: 9881,
+ content_type: 'text/html',
+ show_user: 0,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759046287,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+];
+
+describe('move files from plan', () => {
+ it('1', () => {
+ const messages = transformChunksToMessages(chunks2.slice(0, 1) as MessageChunk[]);
+
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ]);
+ });
+
+ it('2', () => {
+ // Execute incrementally.
+ const stepByStepMessages = transformChunksToMessages(chunks2.slice(1, 2) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ]);
+
+ // Execute in one pass.
+ const onceMessages = transformChunksToMessages(chunks2.slice(0, 2) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+ it('3', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks2.slice(2, 3) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages(chunks2.slice(0, 3) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('4: add plan', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks2.slice(3, 4) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages(chunks2.slice(0, 4) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('5: add in plan', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks2.slice(4, 5) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages(chunks2.slice(0, 5) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [
+ {
+ id: 461,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ detail: {
+ run_id: '05f254a4-e8e3-475b-9b29-39c56bd1850e',
+ full_content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ tool_calls: [
+ {
+ name: 'web_search',
+ args: {
+ query: '2024-2025 National Housing-Price Outlook and Real Estate Market Analysis',
+ query_num: 15,
+ },
+ id: 'call_dcqb8h8cgwdxts36ig37hid0',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 5178,
+ output_tokens: 42,
+ total_tokens: 5220,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045823,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('6: add files too_call', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks2.slice(5, 6) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [
+ {
+ id: 461,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ detail: {
+ run_id: '05f254a4-e8e3-475b-9b29-39c56bd1850e',
+ full_content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ tool_calls: [
+ {
+ name: 'web_search',
+ args: {
+ query: '2024-2025 National Housing-Price Outlook and Real Estate Market Analysis',
+ query_num: 15,
+ },
+ id: 'call_dcqb8h8cgwdxts36ig37hid0',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 5178,
+ output_tokens: 42,
+ total_tokens: 5220,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045823,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages(chunks2.slice(0, 6) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [
+ {
+ id: 461,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ detail: {
+ run_id: '05f254a4-e8e3-475b-9b29-39c56bd1850e',
+ full_content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ tool_calls: [
+ {
+ name: 'web_search',
+ args: {
+ query: '2024-2025 National Housing-Price Outlook and Real Estate Market Analysis',
+ query_num: 15,
+ },
+ id: 'call_dcqb8h8cgwdxts36ig37hid0',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 5178,
+ output_tokens: 42,
+ total_tokens: 5220,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045823,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 538,
+ role: 'assistant',
+ // Update type.
+ type: 'agent_result_delivery',
+ content: '',
+ detail: {
+ run_id: 'a3a48e81-3949-421a-a2e9-244dd3d335b6',
+ tool: 'agent_result_delivery',
+ status: 'pending',
+ param: {
+ attachments: [
+ '/workspace/national-housing-price-outlook-report.md',
+ '/workspace/housing-price-trend-1.png',
+ '/workspace/year-over-year-housing-price-change.png',
+ '/workspace/housing-price-comparison-by-city-tier.png',
+ ],
+ },
+ action: 'Calling agent_result_delivery',
+ action_content: '',
+ },
+ step_id: '',
+ timestamp: 1759046286,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+
+ it('7: add files tool_result', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks2.slice(6, 7) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [
+ {
+ id: 461,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ detail: {
+ run_id: '05f254a4-e8e3-475b-9b29-39c56bd1850e',
+ full_content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ tool_calls: [
+ {
+ name: 'web_search',
+ args: {
+ query: '2024-2025 National Housing-Price Outlook and Real Estate Market Analysis',
+ query_num: 15,
+ },
+ id: 'call_dcqb8h8cgwdxts36ig37hid0',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 5178,
+ output_tokens: 42,
+ total_tokens: 5220,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045823,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 538,
+ role: 'assistant',
+ // Update type.
+ type: 'agent_result_delivery',
+ content: '',
+ detail: {
+ run_id: 'a3a48e81-3949-421a-a2e9-244dd3d335b6',
+ tool: 'agent_result_delivery',
+ status: 'pending',
+ param: {
+ attachments: [
+ '/workspace/national-housing-price-outlook-report.md',
+ '/workspace/housing-price-trend-1.png',
+ '/workspace/year-over-year-housing-price-change.png',
+ '/workspace/housing-price-comparison-by-city-tier.png',
+ ],
+ },
+ action: 'Calling agent_result_delivery',
+ action_content: '',
+ },
+ step_id: '',
+ timestamp: 1759046286,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+
+ const onceMessages = transformChunksToMessages(chunks2.slice(0, 7) as MessageChunk[]);
+
+ const expectedMessages = [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: 457,
+ role: 'user',
+ type: 'text',
+ content: 'Create a 2,000-word nationwide housing-price outlook report. Include chart data and deliver the final document as a PDF.',
+ detail: {},
+ step_id: null,
+ timestamp: 1759045815,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: 458,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ detail: {
+ run_id: '676cd598-ad6b-4b7d-baee-ffc9c9fbf819',
+ full_content:
+ 'I will create a nationwide housing-price outlook report with charts and a PDF document. I will start by preparing a detailed plan.',
+ tool_calls: [
+ {
+ name: 'plan',
+ args: {
+ plans: [
+ {
+ id: '1',
+ content: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ },
+ {
+ id: '2',
+ content: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ },
+ {
+ id: '3',
+ content: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ },
+ {
+ id: '4',
+ content: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ },
+ ],
+ },
+ id: 'call_n8w2s40hxg2rqrlrmd9bzjs1',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 4964,
+ output_tokens: 134,
+ total_tokens: 5098,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 459,
+ role: 'assistant',
+ type: 'text',
+ content:
+ 'I will follow this plan:\n\n1. Search for the latest nationwide housing-price data and market trends\n2. Collect housing-price statistics and create charts\n3. Write a 2,000-word housing-price outlook report\n4. Generate a PDF document containing the charts\n\nYou can interrupt me at any time with new information or changes to the plan.',
+ detail: {},
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 460,
+ role: 'assistant',
+ type: 'plan',
+ content: '',
+ // Add children.
+ children: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ // Add children.
+ children: [
+ {
+ id: 461,
+ role: 'assistant',
+ type: 'text',
+ content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ detail: {
+ run_id: '05f254a4-e8e3-475b-9b29-39c56bd1850e',
+ full_content: 'I will start by searching for the latest nationwide housing-price data and market trends.',
+ tool_calls: [
+ {
+ name: 'web_search',
+ args: {
+ query: '2024-2025 National Housing-Price Outlook and Real Estate Market Analysis',
+ query_num: 15,
+ },
+ id: 'call_dcqb8h8cgwdxts36ig37hid0',
+ type: 'tool_call',
+ },
+ ],
+ usage: {
+ input_tokens: 5178,
+ output_tokens: 42,
+ total_tokens: 5220,
+ input_token_details: {
+ cache_read: 0,
+ },
+ output_token_details: {
+ reasoning: 0,
+ },
+ },
+ },
+ step_id: '',
+ timestamp: 1759045823,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ {
+ id: 539,
+ role: 'assistant',
+ type: 'agent_result_delivery',
+ content: '',
+ detail: {
+ run_id: 'a3a48e81-3949-421a-a2e9-244dd3d335b6',
+ tool: 'agent_result_delivery',
+ result: {
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'agent_result_delivery',
+ id: null,
+ tool_call_id: 'call_uaw9w2zkncg5w67twzmi5lgs',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ attachments: [
+ {
+ filename: 'todo.md',
+ path: '',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/todo.md',
+ size: 244,
+ content_type: 'application/octet-stream',
+ show_user: 0,
+ },
+ {
+ filename: 'housing-price-trend-1.png',
+ path: '/workspace/housing-price-trend-1.png',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/housing-price-trend-1.png',
+ size: 283361,
+ content_type: 'image/png',
+ show_user: 1,
+ },
+ {
+ filename: 'year-over-year-housing-price-change.png',
+ path: '/workspace/year-over-year-housing-price-change.png',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/year-over-year-housing-price-change.png',
+ size: 184938,
+ content_type: 'image/png',
+ show_user: 1,
+ },
+ {
+ filename: 'housing-price-comparison-by-city-tier.png',
+ path: '/workspace/housing-price-comparison-by-city-tier.png',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/housing-price-comparison-by-city-tier.png',
+ size: 124350,
+ content_type: 'image/png',
+ show_user: 1,
+ },
+ {
+ filename: 'national-housing-price-outlook-report.md',
+ path: '/workspace/national-housing-price-outlook-report.md',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/national-housing-price-outlook-report.md',
+ size: 7616,
+ content_type: 'application/octet-stream',
+ show_user: 1,
+ },
+ {
+ filename: 'report_style.css',
+ path: '',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/report_style.css',
+ size: 2005,
+ content_type: 'text/css',
+ show_user: 0,
+ },
+ {
+ filename: 'national-housing-price-outlook-report.html',
+ path: '',
+ url: 'https://test-app.lingyiwanwu.net/boway/sandbox/ibtfuuv0rnb0p4r7ipdso-ecc9fd8d/user_attachments/national-housing-price-outlook-report.html',
+ size: 9881,
+ content_type: 'text/html',
+ show_user: 0,
+ },
+ ],
+ // Preserve param, action, and action_content from tool_call.
+ param: {
+ attachments: [
+ '/workspace/national-housing-price-outlook-report.md',
+ '/workspace/housing-price-trend-1.png',
+ '/workspace/year-over-year-housing-price-change.png',
+ '/workspace/housing-price-comparison-by-city-tier.png',
+ ],
+ },
+ action: 'Calling agent_result_delivery',
+ action_content: '',
+ },
+ step_id: '',
+ timestamp: 1759046287,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ // Add children.
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Search for the latest nationwide housing-price data and market trends',
+ status: 'running',
+ started_at: 1759045820906,
+ },
+ {
+ id: '2',
+ title: 'Collect housing-price statistics and create charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '3',
+ title: 'Write a 2,000-word housing-price outlook report',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ {
+ id: '4',
+ title: 'Generate a PDF document containing the charts',
+ status: 'pending',
+ started_at: 1759045820906,
+ },
+ ],
+ },
+ step_id: '',
+ timestamp: 1759045820,
+ is_llm_message: false,
+ session_id: '0f92702c01d44411',
+ // Add isLast.
+ isLast: true,
+ },
+ ],
+ },
+ ];
+
+ expect(stepByStepMessages).toEqual(expectedMessages);
+ expect(onceMessages).toEqual(expectedMessages);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/plan.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/plan.test.ts
new file mode 100644
index 0000000..4d894c7
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/plan.test.ts
@@ -0,0 +1,1461 @@
+import { describe, it, expect } from 'vitest';
+import { pptChunks } from '../mock';
+import { getLoadingMessage } from '../../utils';
+import { MessageChunk } from '@/types';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+
+describe('transformChunksToMessages', () => {
+ it('1', () => {
+ const messages = transformChunksToMessages(pptChunks.slice(0, 1) as MessageChunk[]);
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [pptChunks[0] as MessageChunk],
+ },
+ getLoadingMessage(),
+ ]);
+ });
+
+ it('2', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ },
+ ] as MessageChunk[],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ getLoadingMessage(),
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('3', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('4', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('5', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'tool_call',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('6', () => {
+ const messages = transformChunksToMessages(pptChunks.slice(5, 6) as MessageChunk[], [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784071572_753e6987',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ tool: 'choose_and_read_template_pptx_info',
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ result: {
+ content:
+ '{"content": "[\\n {\\n \\"slide_number\\": 1,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"be179f3b\\": [\\n \\"01AI Presentation\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 2,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"83b517ce\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"682a9d2a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"64b215f0\\": [\\n \\"04\\"\\n ],\\n \\"19a37359\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"1aa366ef\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"db61c16c\\": [\\n \\"02\\"\\n ],\\n \\"9dd5986c\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"eb5d339f\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"8b642dcc\\": [\\n \\"03\\"\\n ],\\n \\"9e319fcb\\": [\\n \\"This is the content of the remarks\\"\\n ],\\n \\"ee8da08a\\": [\\n \\"This is the paragraph title1\\"\\n ],\\n \\"f2a48842\\": [\\n \\"01\\"\\n ],\\n \\"8351ea2e\\": [\\n \\"Contents\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 3,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"a9775c2a\\": [\\n \\"The introduction to the 101st.\\"\\n ],\\n \\"722f74e7\\": [\\n \\"“01.AI.”• Digitalization, based on zero and one, to connect and manage everything with intelligence, and to promote digital transformation and intellectual social building.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 4,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"f9ee6954\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platform, which will effectively help to raise human productivity and thus create huge economic and social values with a strong technological vision and artificial intelligence belief, and zero things dedicated to building a whole new one AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"c15b6480\\": [\\n \\"Text Titles\\"\\n ],\\n \\"a34fe09f\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"dcfced38\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"5655a992\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"9f58fe6c\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"4a064350\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"23129021\\": [\\n \\"AI 2.0 Platform\\"\\n ],\\n \\"96cc470d\\": [\\n \\"With a strong technological vision and artificial intelligence, 01.AI things are dedicated to building a new one. AI 2.0Platforms that will effectively help to raise human productivity and thus create significant economic and social values\\"\\n ],\\n \\"309ca158\\": [\\n \\"AI 2.0 Platform\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 5,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"eae66713\\": [\\n \\"The Philosophy and Maths Meaning of 01\\"\\n ],\\n \\"14b01155\\": [\\n \\"Zero Origin and Development\\"\\n ],\\n \\"4b8d5330\\": [\\n \\"The symbolism of the first.\\"\\n ],\\n \\"e1543e6b\\": [\\n \\"Zero and one in mathematics.\\"\\n ],\\n \\"a1b45b87\\": [\\n \\"The concept of zero is rooted in ancient civilization, where Indian mathematicians use zero as an independent digital invention to promote the improvement of the mathematical system and scientific progress.\\"\\n ],\\n \\"7d8cb902\\": [\\n \\"The first symbol of unity and starting point of a number is the beginning and multiplication of the natural number, representing the unity of the whole and the source of all things.\\"\\n ],\\n \\"75ef77d4\\": [\\n \\"The zero and one are mathematical building blocks, zero are plus and minus elements of identity, and one is the multiplication core, which forms the basis of the digital system and facilitates computing and information processing.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 6,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"fc354462\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"81c521dc\\": [\\n \\"Binary system base\\"\\n ],\\n \\"c9ff1628\\": [\\n \\"How computers operate with zero and one.\\"\\n ],\\n \\"477c00b4\\": [\\n \\"Zero Encoding and Data Storage\\"\\n ],\\n \\"903bf9c9\\": [\\n \\"1\\"\\n ],\\n \\"9c00c483\\": [\\n \\"The binary system, which expresses all data and instructions in zero and one or two states, is the core language of computers and digital equipment, simplifys circuit design and ensures accurate data transmission.\\"\\n ],\\n \\"131611a9\\": [\\n \\"2\\"\\n ],\\n \\"7fe0b43b\\": [\\n \\"Computers convert information to binary codes, perform arithmetic and logical operations through the logical door, and achieve complex data processing and program execution.\\"\\n ],\\n \\"52f74783\\": [\\n \\"3\\"\\n ],\\n \\"ff45f2a8\\": [\\n \\"The 01 code converts information to binary format suitable for storage and transmission, and common code is availableASCIIandUnicode,Storage devices preserve data through charge change.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 7,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"c5d8ade1\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"b0993f80\\": [\\n \\"The way things are digitally realized.\\"\\n ],\\n \\"5e3209a4\\": [\\n \\"The seamless connection of entities to the digital world is achieved through sensor collection of data, real-time network transmission, cloud computing storage analysis and smart algorithm decision-making.\\"\\n ],\\n \\"27103ac8\\": [\\n \\"101 applications in the Internet\\"\\n ],\\n \\"71712164\\": [\\n \\"The 01 Code serves as the basis for binary development, ensuring the accurate transmission of data and efficient collaboration of equipment in the Internet of goods, and promoting intelligent home and intelligent urban development.\\"\\n ],\\n \\"de614814\\": [\\n \\"Digital twinning technology profile\\"\\n ],\\n \\"31b55279\\": [\\n \\"The integration of the digital economy is facilitated by the use of zero data combined with three-dimensional modelling to optimize real-time mapping, failure prediction and maintenance of physical equipment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 8,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"dff43bdd\\": [\\n \\"Zero-one applications in information technology\\"\\n ],\\n \\"75d19ee0\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"c4a426df\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"00e037d3\\": [\\n \\"Zero One Technology for Industrial Change\\"\\n ],\\n \\"b4de8dbb\\": [\\n \\"The zero-one technology promotes manufacturing intelligence, precision management in agriculture and digitalization of services, and the re-establishment of industrial chains and high-quality economic development.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 9,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"4b7576d4\\": [\\n \\"The challenges and problems of 01.AI.\\"\\n ],\\n \\"a1ca8c3f\\": [\\n \\"The spread of intelligent life\\"\\n ],\\n \\"f9176f86\\": [\\n \\"The zero one technology promotes intelligent home, intelligent clothing and universal access to intelligent transport systems, improving the accessibility of life and resource efficiency.\\"\\n ],\\n \\"fbec61e5\\": [\\n \\"Technological divide and digital exclusion\\"\\n ],\\n \\"e77ae3d9\\": [\\n \\"The technological divide, which leads to the digital exclusion of some groups, requires the reduction of the urban-rural gap, the upgrading of digital literacy and the expansion of infrastructure to achieve inclusive development in a digital society.\\"\\n ],\\n \\"b9b4bb28\\": [\\n \\"Ethical and legal issues\\"\\n ],\\n \\"67598b22\\": [\\n \\"Development of zero-sum objects presents ethical and legal challenges, requiring the establishment of regulatory and legal frameworks that guarantee the legitimate use of data, the transparency of artificial intelligence and a rational response to the effects of employment.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 10,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"5144e569\\": [\\n \\"The future of 01.AI.\\"\\n ],\\n \\"db5ff530\\": [\\n \\"Integration of artificial intelligence with the one technology\\"\\n ],\\n \\"2e7e6eec\\": [\\n \\"The zero one technology provides the basic data expression and computing of artificial intelligence and promotes the upgrading of the intelligence system through the chain.\\"\\n ],\\n \\"7f245244\\": [\\n \\"Impact of quantum calculations on the 101 system\\"\\n ],\\n \\"bdbde1cb\\": [\\n \\"Quantum computing has broken the traditional 01 limit, promoting the development of classic Zero-one technologies and quantum computing in combination, and expanding the boundaries of computing capabilities.\\"\\n ],\\n \\"0a629dfc\\": [\\n \\"Smart cities and the construction of intelligent societies\\"\\n ],\\n \\"e2197e6e\\": [\\n \\"Zero-one technology drives the smart city core system to achieve intelligent management and improve quality of life and the efficiency of urban operations.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 11,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"61d550f6\\": [\\n \\"Summary and reflection\\"\\n ],\\n \\"86dba575\\": [\\n \\"A review of the core values of 01.AI.\\"\\n ],\\n \\"f4abbd51\\": [\\n \\"The zero-sum potential of the basic unit of information is reflected in the underlying concepts of the digital age, driving the digital transformation and industrial upgrading.\\"\\n ],\\n \\"4428e62e\\": [\\n \\"Digital thinking for the future\\"\\n ],\\n \\"cd06c39d\\": [\\n \\"The integration of emerging technologies has driven the expansion of the application of 101 things, and digital transformations have transformed productive life, with attention to equity and data security.\\"\\n ]\\n }\\n }\\n },\\n {\\n \\"slide_number\\": 12,\\n \\"replace_content\\": {\\n \\"text\\": {\\n \\"33017331\\": [\\n \\"Thank you.\\"\\n ]\\n }\\n }\\n }\\n]", "content_type": "markdown", "status": "success", "template_path": "/tmp/doc/uploaded_template_109f091d397042ed_01.AI.-Light.pptx"}',
+ additional_kwargs: {},
+ response_metadata: {},
+ type: 'tool',
+ name: 'choose_and_read_template_pptx_info',
+ id: '47c60773-a8ec-42a4-966f-7fc775888fcd',
+ tool_call_id: 'tooluse_Zk6nBNkWQSSo41jwhYZKPQ',
+ artifact: null,
+ status: 'success',
+ },
+ status: 'success',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071578,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('update plan', () => {
+ const messages = transformChunksToMessages(
+ [
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'success',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ },
+ ],
+ [
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ],
+ );
+ expect(messages).toEqual([
+ {
+ role: 'user',
+ messages: [
+ {
+ id: '1756784044508_fcb97c02',
+ role: 'user',
+ type: 'text',
+ content: 'Presenting 01.AI.',
+ detail: {
+ files: [
+ {
+ uid: '1',
+ status: 'done',
+ key: '01AI Light.pptx',
+ url: 'https://productivity-console-prod-sh.oss-cn-shanghai.aliyuncs.com/ppt_info/01AI Light.pptx',
+ name: '01.AI.-Light.pptx',
+ size: 10000,
+ type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ },
+ ],
+ },
+ timestamp: 1756784044509,
+ },
+ ],
+ },
+ {
+ role: 'assistant',
+ messages: [
+ {
+ id: '1756784056095_43bb5fcc',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I\'ll create a new one for you.PPTIntroduction. I see you\'ve uploaded it."01.AI.-Light.pptx"Template file, I\'ll use this template to make presentations.\n\nLet me analyze the template structure and gather all the relevant information for a comprehensive presentation.PPT。',
+ detail: {
+ attachments: [],
+ intent_type: 'progress_update',
+ scene: 'not_phone',
+ },
+ timestamp: 1756784056099,
+ isLast: true,
+ },
+ {
+ id: '1756784062554_e2988d49',
+ role: 'assistant',
+ type: 'message_notify_user',
+ content:
+ 'I will proceed according to the following plans:\n\n1. Analyse user uploadsPPTTemplate Structure\n2. Collecting details of the company that owns 01.AI.\n3. PlanningPPTContent structure and outline\n4. Create with TemplatePPTSlide\n5. Upload and deliver finalPPTDocumentation\n\nIn the course of my work, you can interrupt me at any time, give me new information or adjust the plan.',
+ detail: {},
+ timestamp: 1756784062558,
+ isLast: true,
+ },
+ {
+ id: '1756784062580_8bd8162a',
+ role: 'assistant',
+ type: 'plan',
+ content: 'Create a company that will make a comprehensive presentation of the 1010th.PPTPresentation, using user-provided templates',
+ children: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'success',
+ started_at: 1756784062580,
+ children: [
+ {
+ id: '1756784066577_7cde3c6f',
+ role: 'assistant',
+ type: 'choose_and_read_template_pptx_info',
+ content: '',
+ detail: {
+ run_id: '981dd306-21b8-4533-ad9e-fdf4d33f22c4',
+ tool: 'choose_and_read_template_pptx_info',
+ status: 'pending',
+ param: {
+ pptx_path: '/workspace/upload/01.AI.-Light.pptx',
+ },
+ action: 'Calling choose_and_read_template_pptx_info',
+ action_content: '',
+ },
+ timestamp: 1756784071547,
+ isLast: true,
+ },
+ ],
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'running',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ children: [],
+ },
+ ],
+ detail: {
+ steps: [
+ {
+ id: '1',
+ title: 'Analyse user uploadsPPTTemplate Structure',
+ status: 'running',
+ started_at: 1756784062580,
+ },
+ {
+ id: '2',
+ title: 'Collecting details of the company that owns 01.AI.',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '3',
+ title: 'PlanningPPTContent structure and outline',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '4',
+ title: 'Create with TemplatePPTSlide',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ {
+ id: '5',
+ title: 'Upload and deliver finalPPTDocumentation',
+ status: 'pending',
+ started_at: 1756784062580,
+ },
+ ],
+ },
+ timestamp: 1756784062588,
+ isLast: true,
+ },
+ ],
+ },
+ ]);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/streaming.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/streaming.test.ts
new file mode 100644
index 0000000..618283e
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/streaming.test.ts
@@ -0,0 +1,139 @@
+import { describe, it, expect } from 'vitest';
+import { streamingChunks } from '../mock';
+import { MessageChunk } from '@/types';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+
+describe('transformChunksToMessages', () => {
+ it('1', () => {
+ const stepByStepMessages = transformChunksToMessages(streamingChunks.slice(0, 1) as MessageChunk[], []);
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 1) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('2', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(1, 2) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 1) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 2) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('3', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(2, 3) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 2) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 3) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('4', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(3, 4) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 3) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 4) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('5', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(4, 5) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 4) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 5) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('6', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(5, 6) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 5) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 6) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('7', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(6, 7) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 6) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 7) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('8', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(7, 8) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 7) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 8) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('9', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(8, 9) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 8) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 9) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('10', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(9, 10) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 9) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 10) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('21', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(20, 21) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 20) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 21) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('22', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(21, 22) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 21) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 22) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('23', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(22, 23) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 22) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 23) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('24', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(23, 24) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 23) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 24) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('25', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ streamingChunks.slice(24, 25) as MessageChunk[],
+ transformChunksToMessages(streamingChunks.slice(0, 24) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(streamingChunks.slice(0, 25) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+});
diff --git a/web/src/hooks/useChat/__tests__/transformChunksToMessages/widget.test.ts b/web/src/hooks/useChat/__tests__/transformChunksToMessages/widget.test.ts
new file mode 100644
index 0000000..160d019
--- /dev/null
+++ b/web/src/hooks/useChat/__tests__/transformChunksToMessages/widget.test.ts
@@ -0,0 +1,1648 @@
+import { describe, it, expect } from 'vitest';
+import { MessageChunk } from '@/types';
+import { findIndex } from 'lodash-es';
+import { transformChunksToMessages } from '../../transformChunksToMessages';
+
+const chunks = [
+ {
+ id: '1764052113555_7f510b94',
+ role: 'user',
+ type: 'text',
+ content: 'I need insurance.',
+ detail: {},
+ timestamp: 1764052113555,
+ trace_id: null,
+ trace_base_url: 'https://trace.example.invalid',
+ step_id: null,
+ is_llm_message: false,
+ session_id: '1d0b58c2c65249eb',
+ },
+ {
+ id: '1764052119298_qftt',
+ role: 'assistant',
+ type: 'text',
+ content: 'I got it.',
+ detail: { streaming: true, run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5' },
+ timestamp: 1764052119298,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052119300_otsz',
+ role: 'assistant',
+ type: 'text',
+ content: 'Yes',
+ detail: { streaming: true, run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5' },
+ timestamp: 1764052119300,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052119301_pu3q',
+ role: 'assistant',
+ type: 'text',
+ content: 'You\'re the one who made it.',
+ detail: { streaming: true, run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5' },
+ timestamp: 1764052119301,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052119302_4g8s',
+ role: 'assistant',
+ type: 'text',
+ content: 'Build a security.',
+ detail: { streaming: true, run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5' },
+ timestamp: 1764052119302,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052119303_86y2',
+ role: 'assistant',
+ type: 'text',
+ content: '- It\'s a risk.',
+ detail: { streaming: true, run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5' },
+ timestamp: 1764052119303,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+ {
+ id: '1764052119303_6yrw',
+ role: 'assistant',
+ type: 'text',
+ content: 'Insurance policy.',
+ detail: { streaming: true, run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5' },
+ timestamp: 1764052119303,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052120586_wisw',
+ role: 'assistant',
+ type: 'text',
+ content: '',
+ detail: {
+ run_id: '6b89d479-4484-4c5d-ad3f-b09627f5a8b5',
+ full_content: 'I\'m here to create an insurance policy for you.',
+ tool_calls: [
+ {
+ name: 'widget_insurance_form',
+ args: {
+ productOptions: [
+ { label: 'Major sickness insurance', value: 'critical_illness', description: 'Protecting the risk of major diseases' },
+ { label: 'Medical insurance', value: 'medical', description: 'Guaranteed medical expenses' },
+ { label: 'Accident insurance', value: 'accident', description: 'Safeguarding the risk of accidental injury' },
+ { label: 'Life insurance', value: 'life', description: 'Risks to life' },
+ ],
+ payYearOptions: [
+ { label: '5Year', value: '5' },
+ { label: '10Year', value: '10' },
+ { label: '15Year', value: '15' },
+ { label: '20Year', value: '20' },
+ { label: '30Year', value: '30' },
+ ],
+ genderOptions: [
+ { label: 'Men', value: 'male' },
+ { label: 'Women', value: 'female' },
+ ],
+ },
+ id: 'toolu_bdrk_016HzYJW3g415Y1X3wDr4jC7',
+ type: 'tool_call',
+ },
+ ],
+ usage: { input_tokens: 0, output_tokens: 348, total_tokens: 348, input_token_details: {} },
+ },
+ timestamp: 1764052120586,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052120686_yidf',
+ role: 'assistant',
+ type: 'widget',
+ content: '',
+ detail: {
+ type: 'done',
+ item_id: '1764052120629_ckp3',
+ widget: {
+ type: 'Card',
+ asForm: true,
+ children: [
+ {
+ children: [
+ { type: 'Title', value: 'Insurance forms', size: 'md' },
+ { type: 'Caption', value: 'Insurance scheme' },
+ {
+ children: [
+ {
+ children: [
+ { type: 'Label', value: 'Products', fieldName: 'plan.product' },
+ {
+ type: 'Select',
+ name: 'plan.product',
+ options: [
+ { value: 'critical_illness', label: 'Major sickness insurance', description: 'Protecting the risk of major diseases' },
+ { value: 'medical', label: 'Medical insurance', description: 'Guaranteed medical expenses' },
+ { value: 'accident', label: 'Accident insurance', description: 'Safeguarding the risk of accidental injury' },
+ { value: 'life', label: 'Life insurance', description: 'Risks to life' },
+ ],
+ placeholder: 'Select the product',
+ },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Guarantee ($)', fieldName: 'plan.sumAssured' },
+ {
+ type: 'Input',
+ name: 'plan.sumAssured',
+ inputType: 'number',
+ required: true,
+ placeholder: 'For example: 300000',
+ },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Years of contributions', fieldName: 'plan.premiumYears' },
+ {
+ type: 'Select',
+ name: 'plan.premiumYears',
+ options: [
+ { value: '5', label: '5Year' },
+ { value: '10', label: '10Year' },
+ { value: '15', label: '15Year' },
+ { value: '20', label: '20Year' },
+ { value: '30', label: '30Year' },
+ ],
+ placeholder: 'Select Years',
+ },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Annual contributions', fieldName: 'plan.cost' },
+ {
+ type: 'Input',
+ name: 'plan.cost',
+ inputType: 'number',
+ required: true,
+ placeholder: 'For example: 300000',
+ },
+ ],
+ type: 'Col',
+ },
+ ],
+ gap: 2,
+ type: 'Col',
+ },
+ { type: 'Divider' },
+ { type: 'Caption', value: 'Information on the insured person' },
+ {
+ children: [
+ {
+ children: [
+ { type: 'Label', value: 'Name', fieldName: 'policyholder.name' },
+ { type: 'Input', name: 'policyholder.name', required: true, placeholder: 'Zhang San' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Cell phone number', fieldName: 'policyholder.phone' },
+ {
+ type: 'Input',
+ name: 'policyholder.phone',
+ inputType: 'tel',
+ required: true,
+ placeholder: '11Cell phone number',
+ },
+ ],
+ type: 'Col',
+ },
+ ],
+ gap: 2,
+ type: 'Col',
+ },
+ { type: 'Divider' },
+ { type: 'Caption', value: 'Information on the insured person' },
+ {
+ children: [
+ {
+ children: [
+ { type: 'Label', value: 'Name', fieldName: 'insured.name' },
+ { type: 'Input', name: 'insured.name', required: true, placeholder: 'Li Siu' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'ID number', fieldName: 'insured.id' },
+ { type: 'Input', name: 'insured.id', required: true, placeholder: '18ID number' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Cell phone number', fieldName: 'insured.phone' },
+ {
+ type: 'Input',
+ name: 'insured.phone',
+ inputType: 'tel',
+ required: true,
+ placeholder: '11Cell phone number',
+ },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Age', fieldName: 'insured.age' },
+ { type: 'Input', name: 'insured.age', inputType: 'number', required: true, placeholder: 'Age' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Gender', fieldName: 'insured.gender' },
+ {
+ type: 'Select',
+ name: 'insured.gender',
+ options: [
+ { value: 'male', label: 'Men' },
+ { value: 'female', label: 'Women' },
+ ],
+ placeholder: 'Sex selection',
+ },
+ ],
+ type: 'Col',
+ },
+ ],
+ gap: 2,
+ type: 'Col',
+ },
+ { type: 'Divider' },
+ { type: 'Caption', value: 'Beneficiaries' },
+ {
+ children: [
+ {
+ children: [
+ { type: 'Label', value: 'Name', fieldName: 'benefit.death.name' },
+ { type: 'Input', name: 'benefit.death.name', required: true, placeholder: 'Name of beneficiary' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Relations with policy-holders', fieldName: 'benefit.death.relation' },
+ {
+ type: 'Input',
+ name: 'benefit.death.relation',
+ required: true,
+ placeholder: 'For example, a spouse/Children/Parents',
+ },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Cell phone number', fieldName: 'benefit.death.phone' },
+ { type: 'Input', name: 'benefit.death.phone', inputType: 'tel', placeholder: 'Cell phone number' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Proportion of beneficiaries%', fieldName: 'benefit.death.share' },
+ {
+ type: 'Input',
+ name: 'benefit.death.share',
+ inputType: 'number',
+ defaultValue: '100',
+ placeholder: 'For example: 100',
+ },
+ ],
+ type: 'Col',
+ },
+ ],
+ gap: 2,
+ type: 'Col',
+ },
+ { type: 'Divider' },
+ { type: 'Caption', value: 'Disabled beneficiaries' },
+ {
+ children: [
+ {
+ children: [
+ { type: 'Label', value: 'Name', fieldName: 'benefit.disability.name' },
+ { type: 'Input', name: 'benefit.disability.name', placeholder: 'Name of beneficiary' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Relations with policy-holders', fieldName: 'benefit.disability.relation' },
+ { type: 'Input', name: 'benefit.disability.relation', placeholder: 'For example, a spouse/Children/Parents' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Cell phone number', fieldName: 'benefit.disability.phone' },
+ { type: 'Input', name: 'benefit.disability.phone', inputType: 'tel', placeholder: 'Cell phone number' },
+ ],
+ type: 'Col',
+ },
+ {
+ children: [
+ { type: 'Label', value: 'Proportion of beneficiaries%', fieldName: 'benefit.disability.share' },
+ { type: 'Input', name: 'benefit.disability.share', inputType: 'number', placeholder: 'For example: 100' },
+ ],
+ type: 'Col',
+ },
+ ],
+ gap: 2,
+ type: 'Col',
+ },
+ ],
+ gap: 3,
+ type: 'Col',
+ },
+ ],
+ size: 'md',
+ confirm: { label: 'Submission of insurance', action: { type: 'insurance.apply', handler: 'server', loadingBehavior: 'auto' } },
+ cancel: { label: 'Cancel', action: { type: 'insurance.cancel', handler: 'server', loadingBehavior: 'auto' } },
+ },
+ copy_text: null,
+ },
+ timestamp: 1764052120686,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: 'de9ecf8da6ea17c86ec0baba87fb10bf',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052124387_rh3q',
+ role: 'assistant',
+ type: 'finish_reason',
+ content: 'Task completed',
+ detail: { reason: 'Task completed', status: 'completed' },
+ timestamp: 1764052124387,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_113941',
+ trace_id: null,
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052138950_9c05c222',
+ role: 'user',
+ type: 'text',
+ content: 'Submission of insurance',
+ detail: {},
+ timestamp: 1764052138973,
+ },
+
+ {
+ id: '1764052143882_3hus',
+ role: 'assistant',
+ type: 'text',
+ content: 'I\'ll bet.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143882,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143883_u7c2',
+ role: 'assistant',
+ type: 'text',
+ content: 'I\'m impressed.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143883,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143884_kyh1',
+ role: 'assistant',
+ type: 'text',
+ content: 'Hand it over.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143884,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143885_dd1e',
+ role: 'assistant',
+ type: 'text',
+ content: 'Table',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143885,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143885_p9cv',
+ role: 'assistant',
+ type: 'text',
+ content: 'Single',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143885,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143886_5coh',
+ role: 'assistant',
+ type: 'text',
+ content: 'How many?',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143886,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143886_52i1',
+ role: 'assistant',
+ type: 'text',
+ content: 'Psychic.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143886,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143887_dmtw',
+ role: 'assistant',
+ type: 'text',
+ content: 'Fill',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143887,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143887_1kr1',
+ role: 'assistant',
+ type: 'text',
+ content: 'Not',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143887,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143887_ry68',
+ role: 'assistant',
+ type: 'text',
+ content: 'Fill out. Please.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143887,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143888_xeel',
+ role: 'assistant',
+ type: 'text',
+ content: 'You\'re done.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143888,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052143888_61fr',
+ role: 'assistant',
+ type: 'text',
+ content: 'Yes.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052143888,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144698_kcz3',
+ role: 'assistant',
+ type: 'text',
+ content: 'Here.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144698,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144699_gidb',
+ role: 'assistant',
+ type: 'text',
+ content: 'After the message',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144699,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144699_buzh',
+ role: 'assistant',
+ type: 'text',
+ content: 'Resubmission:\n\n**',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144699,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144700_stxz',
+ role: 'assistant',
+ type: 'text',
+ content: 'Throw!',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144700,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144701_heus',
+ role: 'assistant',
+ type: 'text',
+ content: 'The bodyguard.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144701,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144701_suds',
+ role: 'assistant',
+ type: 'text',
+ content: 'Case:**\n- Production',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144701,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144702_oesq',
+ role: 'assistant',
+ type: 'text',
+ content: 'Practising Name',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144702,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144703_3mbn',
+ role: 'assistant',
+ type: 'text',
+ content: 'Claims',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144703,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144703_vh2n',
+ role: 'assistant',
+ type: 'text',
+ content: '\n- Guarantee\n- Hand it over.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144703,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144704_0unn',
+ role: 'assistant',
+ type: 'text',
+ content: 'Years of fees\n- Year',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144704,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144704_94yz',
+ role: 'assistant',
+ type: 'text',
+ content: 'Hand it over.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144704,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144704_y829',
+ role: 'assistant',
+ type: 'text',
+ content: 'Por',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144704,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144705_731s',
+ role: 'assistant',
+ type: 'text',
+ content: 'Fees',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144705,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144705_abp2',
+ role: 'assistant',
+ type: 'text',
+ content: '\n\n**Throw!',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144705,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144705_kb9g',
+ role: 'assistant',
+ type: 'text',
+ content: 'Security information:**\n-',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144705,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052144705_x06t',
+ role: 'assistant',
+ type: 'text',
+ content: ' Name\n- Hands.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052144705,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145507_s942',
+ role: 'assistant',
+ type: 'text',
+ content: 'Number',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145507,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145508_owhs',
+ role: 'assistant',
+ type: 'text',
+ content: '\n\n**Insured',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145508,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145508_esaf',
+ role: 'assistant',
+ type: 'text',
+ content: 'Person information:**\n- ',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145508,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145509_wtkl',
+ role: 'assistant',
+ type: 'text',
+ content: 'Name\n- Body',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145509,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145510_lluo',
+ role: 'assistant',
+ type: 'text',
+ content: 'Certificate number\n- Cell phone.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145510,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145510_evyu',
+ role: 'assistant',
+ type: 'text',
+ content: 'No, no, no.\n- Age',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145510,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145511_ds77',
+ role: 'assistant',
+ type: 'text',
+ content: '\n- Gender\n\n**Body',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145511,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145512_ug4a',
+ role: 'assistant',
+ type: 'text',
+ content: 'I\'m sorry.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145512,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145513_1r0j',
+ role: 'assistant',
+ type: 'text',
+ content: 'Beneath me:',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145513,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145513_ds57',
+ role: 'assistant',
+ type: 'text',
+ content: '**\n- Name',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145513,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145513_jhu9',
+ role: 'assistant',
+ type: 'text',
+ content: '\n- and',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145513,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145514_9qba',
+ role: 'assistant',
+ type: 'text',
+ content: 'Throw!',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145514,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145514_xikw',
+ role: 'assistant',
+ type: 'text',
+ content: 'Security relations\n- Hands.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145514,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145514_xi2r',
+ role: 'assistant',
+ type: 'text',
+ content: 'Number',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145514,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145515_d5ve',
+ role: 'assistant',
+ type: 'text',
+ content: '\n\n**Body',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145515,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052145515_wxzk',
+ role: 'assistant',
+ type: 'text',
+ content: 'Disability',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052145515,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146351_7glk',
+ role: 'assistant',
+ type: 'text',
+ content: 'Yes.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146351,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146352_8075',
+ role: 'assistant',
+ type: 'text',
+ content: 'Beneath me:**\n- ',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146352,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146353_t28l',
+ role: 'assistant',
+ type: 'text',
+ content: 'Name\n- Insurance',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146353,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146353_6ev6',
+ role: 'assistant',
+ type: 'text',
+ content: 'Human relations\n- Cell phone.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146353,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146354_6inr',
+ role: 'assistant',
+ type: 'text',
+ content: 'No, no, no.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146354,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146355_fnmz',
+ role: 'assistant',
+ type: 'text',
+ content: '\n- Yes.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146355,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146355_cdto',
+ role: 'assistant',
+ type: 'text',
+ content: 'Gaines',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146355,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146356_ypht',
+ role: 'assistant',
+ type: 'text',
+ content: 'Example\n\nPlease.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146356,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146357_0kdh',
+ role: 'assistant',
+ type: 'text',
+ content: 'You\'re back.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146357,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146357_onfx',
+ role: 'assistant',
+ type: 'text',
+ content: 'Return Form',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146357,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146357_2tzj',
+ role: 'assistant',
+ type: 'text',
+ content: 'Fill',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146357,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146358_dnxc',
+ role: 'assistant',
+ type: 'text',
+ content: 'Write a full letter.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146358,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146358_r8ta',
+ role: 'assistant',
+ type: 'text',
+ content: 'I\'ll...',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146358,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146358_8lu8',
+ role: 'assistant',
+ type: 'text',
+ content: 'Help you.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146358,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146359_twuu',
+ role: 'assistant',
+ type: 'text',
+ content: '(c) Processing of applications for insurance.',
+ detail: { streaming: true, run_id: '6b264749-7747-4743-ab4a-84694530b406' },
+ timestamp: 1764052146359,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146364_4c2x',
+ role: 'assistant',
+ type: 'text',
+ content: '',
+ detail: {
+ run_id: '6b264749-7747-4743-ab4a-84694530b406',
+ full_content:
+ 'I note that you have submitted several entries that are not filled out. Please complete the following information before submitting:\n\n**Insurance schemes:**\n- Product name\n- Guarantee\n- Years of contributions\n- Annual contributions\n\n**Information on the insured person:**\n- Name\n- Cell phone number\n\n**Information on insured persons:**\n- Name\n- ID number\n- Cell phone number\n- Age\n- Gender\n\n**Beneficiaries of death:**\n- Name\n- Relations with policy-holders\n- Cell phone number\n\n**Beneficiaries with disabilities:**\n- Name\n- Relations with policy-holders\n- Cell phone number\n- Proportion of beneficiaries\n\nPlease return the form and complete the information. I will help you with the application.',
+ usage: { input_tokens: 0, output_tokens: 226, total_tokens: 226, input_token_details: {} },
+ },
+ timestamp: 1764052146364,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: '586e347f127813ee1733727170d013d4',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052146393_pio7',
+ role: 'assistant',
+ type: 'finish_reason',
+ content: 'Task completed',
+ detail: { reason: 'Task completed', status: 'completed' },
+ timestamp: 1764052146393,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_139047',
+ trace_id: null,
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ { id: '1764052150602_5e4c7556', role: 'user', type: 'text', content: 'Cancel', detail: {}, timestamp: 1764052150624 },
+
+ {
+ id: '1764052155324_x4sd',
+ role: 'assistant',
+ type: 'text',
+ content: 'Okay.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155324,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155325_3ej3',
+ role: 'assistant',
+ type: 'text',
+ content: 'Yes, it is.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155325,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155326_5fbu',
+ role: 'assistant',
+ type: 'text',
+ content: 'Cancel the insurance application.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155326,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155326_or4q',
+ role: 'assistant',
+ type: 'text',
+ content: 'Please.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155326,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155327_x0yc',
+ role: 'assistant',
+ type: 'text',
+ content: 'You\'re the one who\'s got to be here.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155327,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155328_zwvb',
+ role: 'assistant',
+ type: 'text',
+ content: ' And the ',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155328,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155328_nr5u',
+ role: 'assistant',
+ type: 'text',
+ content: 'Back',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155328,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155328_zodu',
+ role: 'assistant',
+ type: 'text',
+ content: 'We need to pitch.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155328,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155329_532s',
+ role: 'assistant',
+ type: 'text',
+ content: 'Guarantee or',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155329,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155329_yun0',
+ role: 'assistant',
+ type: 'text',
+ content: 'Yes.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155329,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155329_pvt3',
+ role: 'assistant',
+ type: 'text',
+ content: 'Ren.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155329,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155330_dlnl',
+ role: 'assistant',
+ type: 'text',
+ content: 'Ho Bao!',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155330,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155531_7oo1',
+ role: 'assistant',
+ type: 'text',
+ content: 'Risk-related',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155531,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155532_uwg0',
+ role: 'assistant',
+ type: 'text',
+ content: 'Problem, whatever.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155532,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155533_35iy',
+ role: 'assistant',
+ type: 'text',
+ content: 'You can sue me now.',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155533,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155534_3ici',
+ role: 'assistant',
+ type: 'text',
+ content: '- Say something!',
+ detail: { streaming: true, run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43' },
+ timestamp: 1764052155534,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: 'content',
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155534_ylw0',
+ role: 'assistant',
+ type: 'text',
+ content: '',
+ detail: {
+ run_id: '8331e6b3-e7e0-46f2-bd64-f02308ed0b43',
+ full_content: 'You can tell me if you need insurance or any insurance-related issues later!',
+ usage: { input_tokens: 0, output_tokens: 45, total_tokens: 45, input_token_details: {} },
+ },
+ timestamp: 1764052155534,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: '430cdbf4ef9dd3e39ec4a68fdc30304b',
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+
+ {
+ id: '1764052155561_0rp5',
+ role: 'assistant',
+ type: 'finish_reason',
+ content: 'Task completed',
+ detail: { reason: 'Task completed', status: 'completed' },
+ timestamp: 1764052155561,
+ session_id: '1d0b58c2c65249eb',
+ task_id: '1d0b58c2c65249eb_150698',
+ trace_id: null,
+ field_name: null,
+ trace_base_url: 'https://trace.example.invalid',
+ },
+];
+
+describe('transformChunksToMessages', () => {
+ it('1', () => {
+ const stepByStepMessages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[], []);
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('2', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(1, 2) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, 1) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 2) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('3', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(2, 3) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, 2) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 3) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('8', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(8, 9) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, 8) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 9) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+ it('9', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(9, 10) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, 9) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 10) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('12', () => {
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(11, 12) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, 11) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, 12) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('user message 2 - 1', () => {
+ const index = findIndex(chunks, (chunk) => chunk.role === 'user', 2);
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(index - 1, index) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, index - 1) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, index) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('user message 2 - 2', () => {
+ const index = findIndex(chunks, (chunk) => chunk.role === 'user', 2);
+ console.log('index', index);
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(index, index + 1) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, index) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, index + 1) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+
+ it('user message 3', () => {
+ const index = chunks.findLastIndex((chunk) => chunk.role === 'user');
+ const stepByStepMessages = transformChunksToMessages(
+ chunks.slice(index - 1, index) as MessageChunk[],
+ transformChunksToMessages(chunks.slice(0, index - 1) as MessageChunk[]),
+ );
+ const onceMessages = transformChunksToMessages(chunks.slice(0, index) as MessageChunk[]);
+
+ expect(stepByStepMessages).toEqual(onceMessages);
+ });
+});
diff --git a/web/src/hooks/useChat/core/__tests__/README.md b/web/src/hooks/useChat/core/__tests__/README.md
new file mode 100644
index 0000000..cd4b09d
--- /dev/null
+++ b/web/src/hooks/useChat/core/__tests__/README.md
@@ -0,0 +1,77 @@
+# Chat Hook Tests
+
+This directory covers the hooks that parse Agent events, send and resume
+requests, load sessions, and recover interrupted streams.
+
+## Coverage Areas
+
+### `useChunkParser.test.ts`
+
+- Parses valid chunks and ignores invalid JSON.
+- Batches pending chunks into the store.
+- Replaces temporary user-message chunks with server chunks.
+- Handles archived sessions in page and embedded modes.
+- Executes client tools and returns `client_tool_result` when required.
+
+### `useSendContinue.test.ts`
+
+- Sends `send-continue` with the last real chunk ID.
+- Prevents duplicate requests.
+- Adds and removes temporary loading chunks.
+- Handles offline, aborted, failed, and interrupted requests.
+- Enforces retry limits and completion callbacks.
+
+### `useSend.test.ts`
+
+- Sends messages, server actions, and client tool results.
+- Creates sessions in page and embedded modes.
+- Includes files, selected models, metadata, and `interrupt_data`.
+- Handles concurrent sends, aborts, offline state, and retries.
+- Keeps sender state consistent after completion or failure.
+
+### `useSessionManager.test.ts`
+
+- Cleans timers, requests, and pending chunks from the previous session.
+- Loads and resumes active sessions.
+- Avoids stale updates when `sessionId` changes during a request.
+- Skips automatic loading in embedded or preview mode.
+- Handles empty, inactive, failed, and unmounted states.
+
+### `useSSEHandler.test.ts`
+
+- Consumes SSE streams and flushes batched chunks.
+- Clears timers when a stream completes or fails.
+- Distinguishes user aborts from recoverable interruptions.
+- Retries only unfinished assistant messages.
+- Handles empty streams and missing messages safely.
+
+### `useNetworkRecovery.test.ts`
+
+- Resumes unfinished sessions from the last assistant chunk.
+- Aborts active connections when the network goes offline.
+- Debounces `online`, `visibilitychange`, `focus`, and `pageshow` recovery.
+- Handles back-forward cache restoration.
+- Cleans event listeners and retry timers on unmount.
+
+## Running Tests
+
+From the `web` directory:
+
+```bash
+pnpm test --run
+```
+
+Run one suite by name:
+
+```bash
+pnpm test useChunkParser
+pnpm test useSendContinue
+pnpm test useSend
+pnpm test useSessionManager
+pnpm test useSSEHandler
+pnpm test useNetworkRecovery
+```
+
+The tests use Vitest fake timers, mocked Fetch responses, async SSE iterators,
+and stable mock store objects. When a test schedules timers, advance or drain
+them explicitly to avoid order-dependent results.
diff --git a/web/src/hooks/useChat/core/__tests__/useChunkParser.test.ts b/web/src/hooks/useChat/core/__tests__/useChunkParser.test.ts
new file mode 100644
index 0000000..7a59889
--- /dev/null
+++ b/web/src/hooks/useChat/core/__tests__/useChunkParser.test.ts
@@ -0,0 +1,875 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useChunkParser } from '../useChunkParser';
+import type { MessageChunk, ClientToolCallChunk, EventErrorChunk } from '@/types';
+import { FAKE_CHUNK_PREFIX, FAKE_USER_MESSAGE_PREFIX } from '../../utils/constants';
+
+// Mock Dependency
+vi.mock('@/sdk', () => ({
+ executeClientToolCall: vi.fn(),
+}));
+
+vi.mock('@/utils/json', () => ({
+ isJsonString: vi.fn((str: string) => {
+ try {
+ JSON.parse(str);
+ return true;
+ } catch {
+ return false;
+ }
+ }),
+}));
+
+vi.mock('@/utils/eventBus', () => ({
+ default: {
+ emit: vi.fn(),
+ },
+}));
+
+const { mockAxiosGet } = vi.hoisted(() => ({
+ mockAxiosGet: vi.fn(),
+}));
+
+vi.mock('@/services/request', async (importOriginal) => {
+ const actual = await importOriginal();
+ const axios = {
+ post: vi.fn(),
+ get: mockAxiosGet,
+ };
+ return {
+ ...actual,
+ axios,
+ default: axios,
+ };
+});
+
+vi.mock('@/store', async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useRequestClient: vi.fn(),
+ };
+});
+
+// Import mock Post Module
+import { useRequestClient } from '@/store';
+import { createRequestClient } from '@/services/requestClient';
+import { executeClientToolCall } from '@/sdk';
+import eventBus from '@/utils/eventBus';
+
+describe('useChunkParser', () => {
+ let mockStoreApi: any;
+ let currentMessageRef: any;
+ let mockState: any;
+
+ beforeEach(() => {
+ // Reset All mock
+ vi.clearAllMocks();
+
+ // Create Lasting mock Functions
+ mockState = {
+ agentId: 'test-agent',
+ sessionId: 'session-123',
+ instanceId: 'page',
+ requestConfig: {
+ extraHeaders: {},
+ },
+ sessionConfig: { autoRetryOnArchive: false, enableRouting: true, enableSessionLoading: true },
+ chunks: [],
+ addChunks: vi.fn(),
+ setChunks: vi.fn(),
+ setSenderLoading: vi.fn(),
+ addChunk: vi.fn(),
+ resetStore: vi.fn(),
+ setSessionInfo: vi.fn(),
+ setPendingClientToolResult: vi.fn(),
+ onSessionInfoChange: vi.fn(),
+ clientToolHandlers: {},
+ onToolResult: undefined,
+ abortController: null,
+ sessionInfo: { status: 'ACTIVE' },
+ };
+
+ // Create mock storeApi
+ mockStoreApi = {
+ getState: vi.fn(() => mockState),
+ };
+
+ (useRequestClient as any).mockImplementation(() => createRequestClient(mockStoreApi));
+
+ currentMessageRef = { current: 'test message' };
+ });
+
+ describe('Basic functions', () => {
+ it('It should be correct to interpret the work. JSON chunk', async () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const chunk: MessageChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'text',
+ content: 'Hello, world!',
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(chunk));
+ });
+
+ // Verify chunk Added to pendingChunks
+ expect(result.current.pendingChunks.current).toHaveLength(1);
+ expect(result.current.pendingChunks.current[0]).toEqual(chunk);
+ });
+
+ it('It\'s not working. JSON String', async () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ await act(async () => {
+ await result.current.handleChunk('invalid json');
+ });
+
+ // Verify pendingChunks Empty
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ expect(consoleErrorSpy).toHaveBeenCalled();
+
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('addPendingChunks Should be added in bulk chunks Present. store', async () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const chunk1: MessageChunk = { id: '1', role: 'assistant', type: 'text', content: 'chunk1' };
+ const chunk2: MessageChunk = { id: '2', role: 'assistant', type: 'text', content: 'chunk2' };
+
+ // Add More chunks
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(chunk1));
+ await result.current.handleChunk(JSON.stringify(chunk2));
+ });
+
+ expect(result.current.pendingChunks.current).toHaveLength(2);
+
+ // Batch Add
+ act(() => {
+ result.current.addPendingChunks();
+ });
+
+ expect(mockState.addChunks).toHaveBeenCalledWith([chunk1, chunk2]);
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ });
+
+ it('addPendingChunks No, we\'re not. chunks It\'s not supposed to be called. store', () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ act(() => {
+ result.current.addPendingChunks();
+ });
+
+ expect(mockState.addChunks).not.toHaveBeenCalled();
+ });
+
+ });
+
+ describe('user Message Processing', () => {
+ it('It should be filtered out. fake user message and set up senderLoading Yes false', async () => {
+ const mockChunks = [
+ { id: `${FAKE_USER_MESSAGE_PREFIX}123`, role: 'user', content: 'msg1' },
+ { id: '2', role: 'assistant', content: 'reply' },
+ { id: `${FAKE_USER_MESSAGE_PREFIX}456`, role: 'user', content: 'msg2' },
+ ];
+
+ // Settings chunks
+ mockState.chunks = mockChunks;
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const userChunk: MessageChunk = {
+ id: '4',
+ role: 'user',
+ type: 'text',
+ content: 'New user message',
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(userChunk));
+ });
+
+ // The filter is off. fake user message chunks
+ expect(mockState.setChunks).toHaveBeenCalledWith([
+ { id: '2', role: 'assistant', content: 'reply' },
+ { id: '4', role: 'user', type: 'text', content: 'New user message' },
+ ]);
+
+ // Verify Settings senderLoading Yes false
+ expect(mockState.setSenderLoading).toHaveBeenCalledWith(false);
+
+ // Note:user chunk Yes. setChunks , which is processed, will not be added pendingChunks
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ });
+ });
+
+ describe('Session Archive Error Processing (Performance)code 4102)', () => {
+ it('Embedded mode: Should be reset store & Send Back Messages', async () => {
+ mockState.instanceId = 'embedded';
+ mockState.sessionConfig = { autoRetryOnArchive: true, enableRouting: false, enableSessionLoading: false };
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const errorChunk: EventErrorChunk & MessageChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'error',
+ content: '',
+ code: 4102,
+ message: 'Session archived',
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(errorChunk));
+ });
+
+ // Authenticate Reset store
+ expect(mockState.resetStore).toHaveBeenCalled();
+
+ // Verify Sending Event
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_embedded', {
+ content: 'test message',
+ });
+
+ // Verify chunk Not added pendingChunks
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ });
+
+ it('Page Mode: Error message should be added and session status set for archive', async () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const errorChunk: EventErrorChunk & MessageChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'error',
+ content: '',
+ code: 4102,
+ message: 'Session has been archived',
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(errorChunk));
+ });
+
+ // Verify the expected behavior.
+ expect(mockState.addChunk).toHaveBeenCalledWith(
+ expect.objectContaining({
+ role: 'assistant',
+ type: 'error',
+ content: 'Session has been archived',
+ }),
+ );
+
+ // Verify Session Status
+ expect(mockState.setSessionInfo).toHaveBeenCalledWith(
+ expect.objectContaining({
+ status: 'ARCHIVED',
+ }),
+ );
+
+ // Verify chunk Not added pendingChunks
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ });
+
+ it('Default error message should be used message When does not exist', async () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const errorChunk: EventErrorChunk & MessageChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'error',
+ content: '',
+ code: 4102,
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(errorChunk));
+ });
+
+ expect(mockState.addChunk).toHaveBeenCalledWith(
+ expect.objectContaining({
+ content: 'Session is archived',
+ }),
+ );
+ });
+ });
+
+ describe('Client Tool Call Processing', () => {
+ it('Examples of implementation that should be prioritized handler,And pass on the common context.', async () => {
+ const instanceHandler = vi.fn().mockResolvedValue({ source: 'instance' });
+ mockState.clientToolHandlers = { test_tool: instanceHandler };
+ mockState.abortController = new AbortController();
+ (executeClientToolCall as any).mockResolvedValue({ success: true, data: { source: 'instance' } });
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: 'tool-1',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ session_id: 'session-123',
+ detail: {
+ call_id: 'call-1',
+ name: 'test_tool',
+ arguments: { value: 1 },
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ expect(executeClientToolCall).toHaveBeenCalledWith(
+ toolCallChunk,
+ expect.objectContaining({
+ handler: instanceHandler,
+ agentId: 'test-agent',
+ sessionId: 'session-123',
+ signal: mockState.abortController.signal,
+ }),
+ );
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_page', {
+ type: 'client_tool_result',
+ params: {
+ call_id: 'call-1',
+ status: 'completed',
+ output: { source: 'instance' },
+ },
+ });
+ });
+
+ it('It\'s time to abandon the old. session Client tool call', async () => {
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+ const toolCallChunk: ClientToolCallChunk = {
+ id: 'stale-session-tool',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ session_id: 'session-old',
+ detail: {
+ call_id: 'stale-session-call',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ expect(executeClientToolCall).not.toHaveBeenCalled();
+ expect(mockState.setPendingClientToolResult).not.toHaveBeenCalled();
+ expect(eventBus.emit).not.toHaveBeenCalled();
+ });
+
+ it('Received Any Service tool_result from the time triggers with standard fields onToolResult', async () => {
+ mockState.onToolResult = vi.fn();
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+ const toolResultChunk: MessageChunk = {
+ id: 'result-1',
+ role: 'assistant',
+ type: 'tool_result',
+ content: '',
+ session_id: 'chunk-session',
+ detail: {
+ result: {
+ name: 'nested_tool',
+ status: 'completed',
+ result: { value: 42 },
+ },
+ },
+ } as any;
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolResultChunk));
+ });
+
+ expect(mockState.onToolResult).toHaveBeenCalledWith({
+ toolName: 'nested_tool',
+ chunk: toolResultChunk,
+ sessionId: 'chunk-session',
+ status: 'completed',
+ result: { value: 42 },
+ });
+ expect(result.current.pendingChunks.current).toEqual([toolResultChunk]);
+ });
+
+ it('Same. call_id Only execute and send once when arriving repeatedly', async () => {
+ (executeClientToolCall as any).mockResolvedValue({ success: true, data: {} });
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: 'tool-2',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'duplicate-call',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ expect(executeClientToolCall).toHaveBeenCalledTimes(1);
+ expect(eventBus.emit).toHaveBeenCalledTimes(1);
+ });
+
+ it('handler Switch Before Return session Discarding old results and cleaning pending Status', async () => {
+ let resolveTool!: (value: { success: boolean; data: Record }) => void;
+ (executeClientToolCall as any).mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveTool = resolve;
+ }),
+ );
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+ const toolCallChunk: ClientToolCallChunk = {
+ id: 'stale-tool',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ session_id: 'session-123',
+ detail: {
+ call_id: 'stale-call',
+ name: 'slow_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ let handling!: Promise;
+ await act(async () => {
+ handling = result.current.handleChunk(JSON.stringify(toolCallChunk));
+ await Promise.resolve();
+ });
+ mockState.sessionId = 'session-456';
+ await act(async () => {
+ resolveTool({ success: true, data: { stale: true } });
+ await handling;
+ });
+
+ expect(eventBus.emit).not.toHaveBeenCalled();
+ expect(mockState.setPendingClientToolResult).toHaveBeenNthCalledWith(1, true);
+ expect(mockState.setPendingClientToolResult).toHaveBeenLastCalledWith(false);
+ });
+
+ it('Client tool should be executed and wait_for_result Yes true Send results when', async () => {
+ const mockResult = { success: true, data: { result: 'success' } };
+ (executeClientToolCall as any).mockResolvedValue(mockResult);
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-123',
+ name: 'test_tool',
+ arguments: { param: 'value' },
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ expect(mockState.setPendingClientToolResult).toHaveBeenCalledWith(true);
+
+ // Verify the tool callable
+ expect(executeClientToolCall).toHaveBeenCalledWith(toolCallChunk, expect.any(Object));
+
+ // Verify sending result event
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_page', {
+ type: 'client_tool_result',
+ params: {
+ call_id: 'call-123',
+ status: 'completed',
+ output: { result: 'success' },
+ },
+ });
+
+ // Verify chunk Not added pendingChunks
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ });
+
+ it('Should be sent when tool execution fails failed Status', async () => {
+ const mockResult = { success: false, data: { error: 'failed' } };
+ (executeClientToolCall as any).mockResolvedValue(mockResult);
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-456',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_page', {
+ type: 'client_tool_result',
+ params: {
+ call_id: 'call-456',
+ status: 'failed',
+ output: { error: 'failed' },
+ },
+ });
+ });
+
+ it('I think so. wait_for_result Yes false Do not send results from time to time', async () => {
+ const mockResult = { success: true, data: {} };
+ (executeClientToolCall as any).mockResolvedValue(mockResult);
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-789',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: false,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ // Verify the tool callable
+ expect(executeClientToolCall).toHaveBeenCalled();
+
+ // Verify that no result was sent
+ expect(eventBus.emit).not.toHaveBeenCalled();
+ expect(mockState.setPendingClientToolResult).not.toHaveBeenCalled();
+
+ // Verify chunk Not added pendingChunks
+ expect(result.current.pendingChunks.current).toHaveLength(0);
+ });
+ });
+
+ describe('Different. instanceId Processing', () => {
+ it('It should be based on instanceId Send the right event', async () => {
+ mockState.instanceId = 'embedded';
+ mockState.sessionConfig = { autoRetryOnArchive: false };
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-123',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ (executeClientToolCall as any).mockResolvedValue({ success: true, data: {} });
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ // Validation Usage instanceId Event Name
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_embedded', expect.any(Object));
+ });
+ });
+
+ describe('client_tool_result Do not add a network disconnect hint when sending', () => {
+ it('Send client_tool_result should not add offline Hint', async () => {
+ const mockResult = { success: true, data: { result: 'success' } };
+ (executeClientToolCall as any).mockResolvedValue(mockResult);
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '1',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-123',
+ name: 'test_tool',
+ arguments: { param: 'value' },
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ // Verify sending result event
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_page', {
+ type: 'client_tool_result',
+ params: {
+ call_id: 'call-123',
+ status: 'completed',
+ output: { result: 'success' },
+ },
+ });
+
+ // Validation should not add the false message that the network is disconnected from (offline or interrupted)
+ const addChunkCalls = mockState.addChunk.mock.calls;
+ const offlineOrInterruptedCalls = addChunkCalls.filter((call: any[]) => {
+ const chunk = call[0];
+ return (
+ chunk &&
+ chunk.id &&
+ (chunk.id.includes(`${FAKE_CHUNK_PREFIX}offline`) || chunk.id.includes(`${FAKE_CHUNK_PREFIX}interrupted`))
+ );
+ });
+ expect(offlineOrInterruptedCalls).toHaveLength(0);
+ });
+
+ it('Send client_tool_result should not add interrupted Hint', async () => {
+ mockState.instanceId = 'embedded';
+ const mockResult = { success: false, data: { error: 'failed' } };
+ (executeClientToolCall as any).mockResolvedValue(mockResult);
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '2',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-456',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ // Verify sending result event
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_embedded', {
+ type: 'client_tool_result',
+ params: {
+ call_id: 'call-456',
+ status: 'failed',
+ output: { error: 'failed' },
+ },
+ });
+
+ // Verify should not add false messages about network disconnection
+ const addChunkCalls = mockState.addChunk.mock.calls;
+ const networkErrorCalls = addChunkCalls.filter((call: any[]) => {
+ const chunk = call[0];
+ return (
+ chunk &&
+ chunk.id &&
+ (chunk.id.includes(`${FAKE_CHUNK_PREFIX}offline`) ||
+ chunk.id.includes(`${FAKE_CHUNK_PREFIX}interrupted`) ||
+ chunk.id.includes(`${FAKE_CHUNK_PREFIX}retry`))
+ );
+ });
+ expect(networkErrorCalls).toHaveLength(0);
+ });
+
+ it('Send client_tool_result It\'s not time to clean up old false messages.', async () => {
+ const mockResult = { success: true, data: { result: 'success' } };
+ (executeClientToolCall as any).mockResolvedValue(mockResult);
+
+ // Set up some old false messages
+ const oldFakeChunk: MessageChunk = {
+ id: `${FAKE_CHUNK_PREFIX}retry-123456`,
+ role: 'assistant',
+ type: 'live_status',
+ content: 'Old Reconnecting Tip',
+ };
+ mockState.chunks = [oldFakeChunk];
+
+ const { result } = renderHook(() =>
+ useChunkParser({
+ storeApi: mockStoreApi,
+ currentMessageRef,
+ }),
+ );
+
+ const toolCallChunk: ClientToolCallChunk = {
+ id: '3',
+ role: 'assistant',
+ type: 'client_tool_call',
+ content: '',
+ detail: {
+ call_id: 'call-789',
+ name: 'test_tool',
+ arguments: {},
+ status: 'pending',
+ wait_for_result: true,
+ },
+ };
+
+ await act(async () => {
+ await result.current.handleChunk(JSON.stringify(toolCallChunk));
+ });
+
+ // Verify sending result event
+ expect(eventBus.emit).toHaveBeenCalledWith('call_send_page', {
+ type: 'client_tool_result',
+ params: {
+ call_id: 'call-789',
+ status: 'completed',
+ output: { result: 'success' },
+ },
+ });
+
+ // Verify should not be called setChunks To clear old false messages.
+ expect(mockState.setChunks).not.toHaveBeenCalled();
+
+ // Verify should not add a new network disconnect hint
+ const addChunkCalls = mockState.addChunk.mock.calls;
+ const networkErrorCalls = addChunkCalls.filter((call: any[]) => {
+ const chunk = call[0];
+ return (
+ chunk &&
+ chunk.id &&
+ (chunk.id.includes(`${FAKE_CHUNK_PREFIX}offline`) || chunk.id.includes(`${FAKE_CHUNK_PREFIX}interrupted`))
+ );
+ });
+ expect(networkErrorCalls).toHaveLength(0);
+ });
+ });
+});
diff --git a/web/src/hooks/useChat/core/__tests__/useSSEHandler.test.ts b/web/src/hooks/useChat/core/__tests__/useSSEHandler.test.ts
new file mode 100644
index 0000000..994341a
--- /dev/null
+++ b/web/src/hooks/useChat/core/__tests__/useSSEHandler.test.ts
@@ -0,0 +1,835 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useSSEHandler } from '../useSSEHandler';
+import { DEBOUNCE_TIME, RETRY_DELAY, FAKE_CHUNK_PREFIX } from '../../utils/constants';
+
+// Mock react-router-dom
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => vi.fn(),
+ useLocation: () => ({ pathname: '/test', search: '', hash: '', state: null }),
+}));
+
+// Mock Dependency
+vi.mock('@/hooks/useTranslation', () => ({
+ useTranslation: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ 'chatbot.task.connection.interrupted': 'Connection is down, trying to restore...',
+ };
+ return translations[key] || key;
+ },
+ }),
+}));
+
+vi.mock('@ant-design/x-sdk', () => ({
+ XStream: vi.fn(),
+}));
+
+vi.mock('../../utils', async () => {
+ const actual = await vi.importActual('../../utils');
+ return {
+ ...actual,
+ isMessageFinish: vi.fn(),
+ };
+});
+
+// Import mock Post Module
+import { XStream } from '@ant-design/x-sdk';
+import { isMessageFinish } from '../../utils';
+
+describe('useSSEHandler', () => {
+ let mockStoreApi: any;
+ let mockState: any;
+ let debounceTimerRef: any;
+ let retryTimerRef: any;
+ let mockHandleChunk: any;
+ let mockAddPendingChunks: any;
+
+ beforeEach(() => {
+ // Reset All mock
+ vi.clearAllMocks();
+ vi.useFakeTimers();
+
+ // Create Lasting mock Functions
+ mockState = {
+ chunks: [],
+ pipelineMessages: [],
+ pendingClientToolResult: false,
+ addChunk: vi.fn(),
+ setChunks: vi.fn(),
+ };
+
+ // Create mock storeApi
+ mockStoreApi = {
+ getState: vi.fn(() => mockState),
+ };
+
+ debounceTimerRef = { current: 0 };
+ retryTimerRef = { current: null };
+ mockHandleChunk = vi.fn();
+ mockAddPendingChunks = vi.fn();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ describe('Basic functions', () => {
+ it('It should be handled successfully. SSE Stream', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }, { data: '{"id":"2","content":"chunk2"}' }];
+
+ // Mock XStream Returning the heterogeneity
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Verify Call handleChunk
+ expect(mockHandleChunk).toHaveBeenCalledTimes(2);
+ expect(mockHandleChunk).toHaveBeenNthCalledWith(1, '{"id":"1","content":"chunk1"}');
+ expect(mockHandleChunk).toHaveBeenNthCalledWith(2, '{"id":"2","content":"chunk2"}');
+
+ // Verify Call addPendingChunks(Yes. finally Medium)
+ expect(mockAddPendingChunks).toHaveBeenCalled();
+
+ // Verify Call onComplete
+ expect(mockOnComplete).toHaveBeenCalled();
+ });
+
+ it('Batch processing timer should be activated', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const setIntervalSpy = vi.spyOn(window, 'setInterval');
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, vi.fn(), vi.fn());
+ });
+
+ // Verify timer activated
+ expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), DEBOUNCE_TIME);
+
+ // Note: The timer is cleaned up when the flow is finished, so the timer is set here as the flow is processed.
+ // Because finally Blocks clean the timer, so here it is verified that the timer was set (through setInterval Call)
+
+ setIntervalSpy.mockRestore();
+ });
+
+ it('If the timer already exists, it should not be repeated.', async () => {
+ debounceTimerRef.current = 123 as any; // Timer already in place
+
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const setIntervalSpy = vi.spyOn(window, 'setInterval');
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, vi.fn(), vi.fn());
+ });
+
+ // Verify does not create a new timer
+ expect(setIntervalSpy).not.toHaveBeenCalled();
+
+ setIntervalSpy.mockRestore();
+ });
+
+ it('The timer should be cleaned after the flow is finished.', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const clearIntervalSpy = vi.spyOn(window, 'clearInterval');
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ // Set a timer first ID
+ debounceTimerRef.current = 123 as any;
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, vi.fn(), vi.fn());
+ });
+
+ // Validation cleared the timer
+ expect(clearIntervalSpy).toHaveBeenCalledWith(123);
+ expect(debounceTimerRef.current).toBe(0);
+
+ clearIntervalSpy.mockRestore();
+ });
+ });
+
+ describe('AbortError Processing', () => {
+ it('It should be handled right. AbortError', async () => {
+ const abortError = new Error('Aborted');
+ abortError.name = 'AbortError';
+
+ // Mock XStream Throw AbortError
+ const mockStream = async function* () {
+ yield { data: '{"id":"1","content":"chunk1"}' };
+ throw abortError;
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Check logs
+ expect(consoleLogSpy).toHaveBeenCalledWith('SSE stream was aborted in handleResponse');
+
+ // Verify Call onComplete(Not onTimeout)
+ expect(mockOnComplete).toHaveBeenCalled();
+ expect(mockOnTimeout).not.toHaveBeenCalled();
+
+ // Verify does not add a break hint
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+
+ consoleLogSpy.mockRestore();
+ });
+
+ it('Other types of errors should be dealt with.', async () => {
+ const otherError = new Error('Network error');
+
+ // Mock XStream Throw other errors
+ const mockStream = async function* () {
+ throw otherError;
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ // You should throw the mistake.
+ await expect(
+ act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ }),
+ ).rejects.toThrow('Network error');
+ });
+ });
+
+ describe('Sessions not tested and retrying', () => {
+ it('If the session is not completed, the retry should be triggered', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Sets the status of the incomplete session, including old false messages
+ const realChunk = { id: '1', role: 'assistant', type: 'text', content: 'Incomplete' };
+ const oldFakeChunk = {
+ id: `${FAKE_CHUNK_PREFIX}retry-123456`,
+ role: 'assistant',
+ type: 'live_status',
+ content: 'Old Reconnecting Tip',
+ };
+ mockState.chunks = [realChunk, oldFakeChunk];
+ mockState.pipelineMessages = [{ role: 'assistant', messages: [realChunk] }];
+
+ (isMessageFinish as any).mockReturnValue(false); // Not completed
+
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Check logs
+ expect(consoleLogSpy).toHaveBeenCalledWith('SSE stream ended but session not finished');
+ expect(consoleLogSpy).toHaveBeenCalledWith('scheduling retry with send-continue');
+
+ // Verify to clear old false messages first (retain the real message)
+ expect(mockState.setChunks).toHaveBeenCalled();
+ const setChunksCall = mockState.setChunks.mock.calls[0][0];
+ // setChunks Now, here's the one that's receiving. filterFakeChunks Results (array)
+ expect(setChunksCall).toEqual([realChunk]); // Keep only the real message
+
+ // Check added a break hint
+ expect(mockState.addChunk).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: expect.stringContaining(`${FAKE_CHUNK_PREFIX}interrupted-`),
+ role: 'assistant',
+ type: 'live_status',
+ content: 'Connection is down, trying to restore...',
+ }),
+ );
+
+ // Validation settings for retry timers
+ expect(retryTimerRef.current).not.toBeNull();
+
+ // Validation first. onComplete
+ expect(mockOnComplete).toHaveBeenCalled();
+
+ // Push time, trigger retry
+ act(() => {
+ vi.advanceTimersByTime(RETRY_DELAY);
+ });
+
+ // Verify Call onTimeout
+ expect(mockOnTimeout).toHaveBeenCalled();
+
+ // Verify timer cleared
+ expect(retryTimerRef.current).toBeNull();
+
+ consoleLogSpy.mockRestore();
+ });
+
+ it('Wait client_tool_result Time should not trigger a re-test.', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const realChunk = { id: '1', role: 'assistant', type: 'text', content: 'Waiting for client tool' };
+ mockState.chunks = [realChunk];
+ mockState.pipelineMessages = [{ role: 'assistant', messages: [realChunk] }];
+ mockState.pendingClientToolResult = true;
+
+ (isMessageFinish as any).mockReturnValue(false);
+
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ expect(consoleLogSpy).toHaveBeenCalledWith('SSE stream ended while waiting for client tool result');
+ expect(mockState.setChunks).not.toHaveBeenCalled();
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+ expect(retryTimerRef.current).toBeNull();
+ expect(mockOnComplete).toHaveBeenCalled();
+ expect(mockOnTimeout).not.toHaveBeenCalled();
+
+ consoleLogSpy.mockRestore();
+ });
+
+ it('If the session is completed, no retry should be triggered', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Sets the completed session status
+ mockState.chunks = [{ id: '1', role: 'assistant', type: 'text', content: 'Complete' }];
+ mockState.pipelineMessages = [
+ { role: 'assistant', messages: [{ id: '1', role: 'assistant', type: 'finish_reason', content: 'Complete' }] },
+ ];
+
+ (isMessageFinish as any).mockReturnValue(true); // Completed
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Verify does not add a break hint
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+
+ // Validate no retry timer set
+ expect(retryTimerRef.current).toBeNull();
+
+ // Verify only called onComplete
+ expect(mockOnComplete).toHaveBeenCalled();
+ expect(mockOnTimeout).not.toHaveBeenCalled();
+ });
+
+ it('If not chunks,It\'s not supposed to trigger a re-test.', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Set empty chunks
+ mockState.chunks = [];
+ mockState.pipelineMessages = [];
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Verify does not add a break hint
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+
+ // Validate no retry timer set
+ expect(retryTimerRef.current).toBeNull();
+ });
+
+ it('If the last message isn\'t... assistant,It\'s not supposed to trigger a re-test.', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Set the last message to be user
+ mockState.chunks = [{ id: '1', role: 'user', type: 'text', content: 'User message' }];
+ mockState.pipelineMessages = [
+ { role: 'user', messages: [{ id: '1', role: 'user', type: 'text', content: 'User message' }] },
+ ];
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Verify does not add a break hint
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+
+ // Validate no retry timer set
+ expect(retryTimerRef.current).toBeNull();
+ });
+
+ it('Should clear previous retry timers', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Sets the status of incomplete sessions
+ mockState.chunks = [{ id: '1', role: 'assistant', type: 'text', content: 'Incomplete' }];
+ mockState.pipelineMessages = [
+ { role: 'assistant', messages: [{ id: '1', role: 'assistant', type: 'text', content: 'Incomplete' }] },
+ ];
+
+ (isMessageFinish as any).mockReturnValue(false);
+
+ // Set an existing retry timer
+ retryTimerRef.current = 123 as any;
+
+ const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, vi.fn(), vi.fn());
+ });
+
+ // Verify clears the previous timer
+ expect(clearTimeoutSpy).toHaveBeenCalledWith(123);
+
+ clearTimeoutSpy.mockRestore();
+ });
+ });
+
+ describe('Batch Process Timer', () => {
+ it('Timer should be called on a regular basis addPendingChunks', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, vi.fn(), vi.fn());
+ });
+
+ // Push time, trigger timer.
+ act(() => {
+ vi.advanceTimersByTime(DEBOUNCE_TIME);
+ });
+
+ // Verify Call addPendingChunks
+ expect(mockAddPendingChunks).toHaveBeenCalled();
+
+ // Verify timer cleared
+ expect(debounceTimerRef.current).toBe(0);
+ });
+ });
+
+ describe('Border situation', () => {
+ it('It should be empty. SSE Stream', async () => {
+ const mockStream = async function* () {
+ // An empty stream that yields no data.
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, vi.fn(), mockOnComplete);
+ });
+
+ // Verify not called handleChunk
+ expect(mockHandleChunk).not.toHaveBeenCalled();
+
+ // Verify Call addPendingChunks
+ expect(mockAddPendingChunks).toHaveBeenCalled();
+
+ // Verify Call onComplete
+ expect(mockOnComplete).toHaveBeenCalled();
+ });
+
+ it('It should be handled. pipelineMessages Empty', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Set empty pipelineMessages
+ mockState.chunks = [{ id: '1', role: 'assistant', type: 'text', content: 'Test' }];
+ mockState.pipelineMessages = [];
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Validation does not trigger a re-test (because) lastMessage Yes. undefined)
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+ expect(retryTimerRef.current).toBeNull();
+ });
+
+ it('It should be handled. lastMessage Yes undefined Situation', async () => {
+ const mockChunks = [{ data: '{"id":"1","content":"chunk1"}' }];
+
+ const mockStream = async function* () {
+ for (const chunk of mockChunks) {
+ yield chunk;
+ }
+ };
+
+ (XStream as any).mockReturnValue(mockStream());
+
+ const mockResponse = {
+ body: new ReadableStream(),
+ };
+
+ // Settings pipelineMessages But the last message is... undefined
+ mockState.chunks = [{ id: '1', role: 'assistant', type: 'text', content: 'Test' }];
+ mockState.pipelineMessages = [undefined];
+
+ const { result } = renderHook(() =>
+ useSSEHandler({
+ storeApi: mockStoreApi,
+ debounceTimerRef,
+ retryTimerRef,
+ handleChunk: mockHandleChunk,
+ addPendingChunks: mockAddPendingChunks,
+ enableSendContinue: true,
+ }),
+ );
+
+ const mockOnTimeout = vi.fn();
+ const mockOnComplete = vi.fn();
+
+ await act(async () => {
+ await result.current.handleResponse(mockResponse, mockOnTimeout, mockOnComplete);
+ });
+
+ // Validation does not trigger a re-test (because) lastMessage Yes. undefined)
+ expect(mockState.addChunk).not.toHaveBeenCalled();
+ expect(retryTimerRef.current).toBeNull();
+ });
+ });
+});
diff --git a/web/src/hooks/useChat/core/__tests__/useSend.test.ts b/web/src/hooks/useChat/core/__tests__/useSend.test.ts
new file mode 100644
index 0000000..609490e
--- /dev/null
+++ b/web/src/hooks/useChat/core/__tests__/useSend.test.ts
@@ -0,0 +1,1927 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useSend } from '../useSend';
+import type { SendOptions } from '../../index';
+import type { MessageChunk } from '@/types';
+import { FAKE_CHUNK_PREFIX, RETRY_DELAY } from '../../utils/constants';
+
+// Mock Dependency
+const mockNavigate = vi.fn();
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => mockNavigate,
+ useLocation: () => ({ pathname: '/test', search: '', hash: '', state: null }),
+}));
+
+const { mockAxiosPost, mockAxiosGet } = vi.hoisted(() => ({
+ mockAxiosPost: vi.fn(),
+ mockAxiosGet: vi.fn(),
+}));
+
+vi.mock('@/services/request', async (importOriginal) => {
+ const actual = await importOriginal