nitrostack 1.0.72 → 1.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/api-key.js.map +1 -1
- package/dist/auth/client.js.map +1 -1
- package/dist/auth/index.d.ts +2 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/middleware.d.ts +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js.map +1 -1
- package/dist/auth/secure-secret.d.ts +136 -0
- package/dist/auth/secure-secret.d.ts.map +1 -0
- package/dist/auth/secure-secret.js +182 -0
- package/dist/auth/secure-secret.js.map +1 -0
- package/dist/auth/server-metadata.d.ts.map +1 -1
- package/dist/auth/server-metadata.js.map +1 -1
- package/dist/auth/simple-jwt.d.ts +100 -14
- package/dist/auth/simple-jwt.d.ts.map +1 -1
- package/dist/auth/simple-jwt.js +19 -9
- package/dist/auth/simple-jwt.js.map +1 -1
- package/dist/auth/token-store.js +1 -1
- package/dist/auth/token-store.js.map +1 -1
- package/dist/auth/token-validation.js +1 -1
- package/dist/auth/token-validation.js.map +1 -1
- package/dist/cli/commands/build.js +1 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/generate-types.js +12 -12
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/generate.d.ts +8 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +13 -12
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/upgrade.d.ts +10 -0
- package/dist/cli/commands/upgrade.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.js +221 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/app-decorator.d.ts +4 -3
- package/dist/core/app-decorator.d.ts.map +1 -1
- package/dist/core/app-decorator.js +67 -28
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.d.ts +19 -7
- package/dist/core/builders.d.ts.map +1 -1
- package/dist/core/builders.js +15 -8
- package/dist/core/builders.js.map +1 -1
- package/dist/core/component.d.ts +8 -8
- package/dist/core/component.d.ts.map +1 -1
- package/dist/core/component.js +3 -2
- package/dist/core/component.js.map +1 -1
- package/dist/core/config-module.d.ts +11 -4
- package/dist/core/config-module.d.ts.map +1 -1
- package/dist/core/config-module.js +1 -1
- package/dist/core/config-module.js.map +1 -1
- package/dist/core/decorators/cache.decorator.d.ts +9 -9
- package/dist/core/decorators/cache.decorator.d.ts.map +1 -1
- package/dist/core/decorators/cache.decorator.js +3 -3
- package/dist/core/decorators/cache.decorator.js.map +1 -1
- package/dist/core/decorators/health-check.decorator.d.ts +3 -3
- package/dist/core/decorators/health-check.decorator.d.ts.map +1 -1
- package/dist/core/decorators/health-check.decorator.js +2 -2
- package/dist/core/decorators/health-check.decorator.js.map +1 -1
- package/dist/core/decorators/rate-limit.decorator.d.ts +5 -4
- package/dist/core/decorators/rate-limit.decorator.d.ts.map +1 -1
- package/dist/core/decorators/rate-limit.decorator.js +3 -3
- package/dist/core/decorators/rate-limit.decorator.js.map +1 -1
- package/dist/core/decorators.d.ts +47 -29
- package/dist/core/decorators.d.ts.map +1 -1
- package/dist/core/decorators.js +9 -9
- package/dist/core/decorators.js.map +1 -1
- package/dist/core/di/container.d.ts +21 -4
- package/dist/core/di/container.d.ts.map +1 -1
- package/dist/core/di/container.js +11 -7
- package/dist/core/di/container.js.map +1 -1
- package/dist/core/di/injectable.decorator.d.ts +5 -3
- package/dist/core/di/injectable.decorator.d.ts.map +1 -1
- package/dist/core/di/injectable.decorator.js.map +1 -1
- package/dist/core/errors.d.ts +4 -4
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/events/event-emitter.d.ts +3 -3
- package/dist/core/events/event-emitter.d.ts.map +1 -1
- package/dist/core/events/event-emitter.js.map +1 -1
- package/dist/core/events/event.decorator.d.ts +5 -5
- package/dist/core/events/event.decorator.d.ts.map +1 -1
- package/dist/core/events/event.decorator.js +10 -6
- package/dist/core/events/event.decorator.js.map +1 -1
- package/dist/core/events/log-emitter.d.ts +7 -1
- package/dist/core/events/log-emitter.d.ts.map +1 -1
- package/dist/core/events/log-emitter.js.map +1 -1
- package/dist/core/filters/exception-filter.decorator.d.ts +5 -5
- package/dist/core/filters/exception-filter.decorator.d.ts.map +1 -1
- package/dist/core/filters/exception-filter.decorator.js +3 -3
- package/dist/core/filters/exception-filter.decorator.js.map +1 -1
- package/dist/core/filters/exception-filter.interface.d.ts +14 -5
- package/dist/core/filters/exception-filter.interface.d.ts.map +1 -1
- package/dist/core/guards/apikey.guard.d.ts +1 -1
- package/dist/core/guards/apikey.guard.d.ts.map +1 -1
- package/dist/core/guards/guard.interface.d.ts +1 -1
- package/dist/core/guards/guard.interface.d.ts.map +1 -1
- package/dist/core/guards/jwt.guard.d.ts +1 -1
- package/dist/core/guards/jwt.guard.d.ts.map +1 -1
- package/dist/core/guards/oauth.guard.d.ts +1 -1
- package/dist/core/guards/oauth.guard.d.ts.map +1 -1
- package/dist/core/guards/use-guards.decorator.d.ts +3 -3
- package/dist/core/guards/use-guards.decorator.d.ts.map +1 -1
- package/dist/core/guards/use-guards.decorator.js +1 -1
- package/dist/core/guards/use-guards.decorator.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/interceptors/interceptor.decorator.d.ts +4 -4
- package/dist/core/interceptors/interceptor.decorator.d.ts.map +1 -1
- package/dist/core/interceptors/interceptor.decorator.js +2 -2
- package/dist/core/interceptors/interceptor.decorator.js.map +1 -1
- package/dist/core/interceptors/interceptor.interface.d.ts +3 -3
- package/dist/core/interceptors/interceptor.interface.d.ts.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js.map +1 -1
- package/dist/core/middleware/middleware.decorator.d.ts +4 -4
- package/dist/core/middleware/middleware.decorator.d.ts.map +1 -1
- package/dist/core/middleware/middleware.decorator.js +2 -2
- package/dist/core/middleware/middleware.decorator.js.map +1 -1
- package/dist/core/middleware/middleware.interface.d.ts +3 -3
- package/dist/core/middleware/middleware.interface.d.ts.map +1 -1
- package/dist/core/module.d.ts +33 -14
- package/dist/core/module.d.ts.map +1 -1
- package/dist/core/module.js +11 -6
- package/dist/core/module.js.map +1 -1
- package/dist/core/oauth-module.d.ts +9 -3
- package/dist/core/oauth-module.d.ts.map +1 -1
- package/dist/core/oauth-module.js +4 -3
- package/dist/core/oauth-module.js.map +1 -1
- package/dist/core/pipes/pipe.decorator.d.ts +14 -5
- package/dist/core/pipes/pipe.decorator.d.ts.map +1 -1
- package/dist/core/pipes/pipe.decorator.js +2 -2
- package/dist/core/pipes/pipe.decorator.js.map +1 -1
- package/dist/core/pipes/pipe.interface.d.ts +9 -4
- package/dist/core/pipes/pipe.interface.d.ts.map +1 -1
- package/dist/core/prompt.d.ts +13 -4
- package/dist/core/prompt.d.ts.map +1 -1
- package/dist/core/prompt.js +2 -2
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/resource.d.ts +7 -2
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +2 -2
- package/dist/core/resource.js.map +1 -1
- package/dist/core/server.d.ts +49 -3
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +61 -34
- package/dist/core/server.js.map +1 -1
- package/dist/core/tool.d.ts +44 -16
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +19 -6
- package/dist/core/tool.js.map +1 -1
- package/dist/core/transports/discovery-http-server.d.ts +7 -1
- package/dist/core/transports/discovery-http-server.d.ts.map +1 -1
- package/dist/core/transports/discovery-http-server.js.map +1 -1
- package/dist/core/transports/http-server.d.ts +2 -2
- package/dist/core/transports/http-server.d.ts.map +1 -1
- package/dist/core/transports/http-server.js +1 -1
- package/dist/core/transports/http-server.js.map +1 -1
- package/dist/core/transports/streamable-http.d.ts +4 -4
- package/dist/core/transports/streamable-http.d.ts.map +1 -1
- package/dist/core/transports/streamable-http.js +1 -1
- package/dist/core/transports/streamable-http.js.map +1 -1
- package/dist/core/types.d.ts +87 -15
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/widgets/widget-registry.d.ts +2 -2
- package/dist/core/widgets/widget-registry.d.ts.map +1 -1
- package/dist/core/widgets/widget-registry.js +1 -1
- package/dist/core/widgets/widget-registry.js.map +1 -1
- package/dist/testing/index.d.ts +44 -17
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -8
- package/dist/testing/index.js.map +1 -1
- package/dist/ui-next/index.d.ts +1 -1
- package/dist/ui-next/index.d.ts.map +1 -1
- package/dist/ui-next/index.js.map +1 -1
- package/dist/widgets/hooks/useWidgetSDK.d.ts +5 -5
- package/dist/widgets/runtime/WidgetLayout.js.map +1 -1
- package/dist/widgets/sdk.d.ts +5 -5
- package/dist/widgets/sdk.d.ts.map +1 -1
- package/dist/widgets/sdk.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/app/api/auth/fetch-metadata/route.ts +3 -2
- package/src/studio/app/api/auth/register-client/route.ts +3 -2
- package/src/studio/app/api/chat/route.ts +31 -17
- package/src/studio/app/api/health/checks/route.ts +5 -4
- package/src/studio/app/api/init/route.ts +3 -2
- package/src/studio/app/api/ping/route.ts +3 -2
- package/src/studio/app/api/prompts/[name]/route.ts +4 -3
- package/src/studio/app/api/prompts/route.ts +3 -2
- package/src/studio/app/api/resources/[...uri]/route.ts +3 -2
- package/src/studio/app/api/resources/route.ts +3 -2
- package/src/studio/app/api/roots/route.ts +3 -2
- package/src/studio/app/api/sampling/route.ts +3 -2
- package/src/studio/app/api/tools/[name]/call/route.ts +3 -2
- package/src/studio/app/api/tools/route.ts +4 -3
- package/src/studio/app/api/widget-examples/route.ts +5 -4
- package/src/studio/app/auth/callback/page.tsx +3 -2
- package/src/studio/app/chat/page.tsx +481 -105
- package/src/studio/app/health/page.tsx +1 -1
- package/src/studio/app/logs/page.tsx +2 -2
- package/src/studio/app/page.tsx +5 -5
- package/src/studio/app/prompts/page.tsx +2 -2
- package/src/studio/app/settings/page.tsx +3 -2
- package/src/studio/app/tools/page.tsx +3 -3
- package/src/studio/components/LogMessage.tsx +1 -1
- package/src/studio/components/MarkdownRenderer.tsx +245 -348
- package/src/studio/components/Sidebar.tsx +18 -3
- package/src/studio/components/VoiceOrbOverlay.tsx +12 -6
- package/src/studio/components/WidgetErrorBoundary.tsx +48 -0
- package/src/studio/components/WidgetRenderer.tsx +168 -215
- package/src/studio/components/ops/OpsCanvas.tsx +748 -0
- package/src/studio/components/ops/OpsNodeDetailPanel.tsx +150 -0
- package/src/studio/components/ops/OpsSummaryBar.tsx +90 -0
- package/src/studio/components/ops/index.ts +5 -0
- package/src/studio/components/ops/nodes/BaseNode.tsx +65 -0
- package/src/studio/components/ops/nodes/LLMCallNode.tsx +34 -0
- package/src/studio/components/ops/nodes/LLMResponseNode.tsx +33 -0
- package/src/studio/components/ops/nodes/ToolCallNode.tsx +30 -0
- package/src/studio/components/ops/nodes/ToolResultNode.tsx +43 -0
- package/src/studio/components/ops/nodes/UserPromptNode.tsx +34 -0
- package/src/studio/components/ops/nodes/WidgetRenderNode.tsx +23 -0
- package/src/studio/components/ops/nodes/index.ts +8 -0
- package/src/studio/components/tools/ToolsCanvas.tsx +2 -2
- package/src/studio/lib/api.ts +61 -42
- package/src/studio/lib/http-client-transport.ts +2 -2
- package/src/studio/lib/llm-service.ts +126 -47
- package/src/studio/lib/mcp-client.ts +9 -6
- package/src/studio/lib/ops-store.ts +427 -0
- package/src/studio/lib/ops-tracker.ts +416 -0
- package/src/studio/lib/ops-types.ts +164 -0
- package/src/studio/lib/store.ts +8 -11
- package/src/studio/lib/types.ts +228 -38
- package/src/studio/lib/widget-loader.ts +2 -2
- package/templates/typescript-oauth/src/modules/flights/flights.prompts.ts +19 -22
- package/dist/cli/build-widgets.mjs +0 -165
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
4
4
|
import { useStudioStore } from '@/lib/store';
|
|
5
|
+
import { useOpsStore } from '@/lib/ops-store';
|
|
6
|
+
import { opsTracker, clearOpsSession } from '@/lib/ops-tracker';
|
|
7
|
+
import type { LLMProvider } from '@/lib/ops-types';
|
|
5
8
|
import { api } from '@/lib/api';
|
|
6
9
|
import { WidgetRenderer } from '@/components/WidgetRenderer';
|
|
10
|
+
import { WidgetErrorBoundary } from '@/components/WidgetErrorBoundary';
|
|
7
11
|
import { MarkdownRenderer } from '@/components/MarkdownRenderer';
|
|
8
12
|
import { VoiceOrbOverlay, MiniVoiceOrb } from '@/components/VoiceOrbOverlay';
|
|
13
|
+
import { OpsCanvas } from '@/components/ops/OpsCanvas';
|
|
9
14
|
import type { ChatMessage, Tool, ToolCall, Prompt } from '@/lib/types';
|
|
10
15
|
import {
|
|
11
16
|
SparklesIcon,
|
|
@@ -23,16 +28,104 @@ import {
|
|
|
23
28
|
EllipsisVerticalIcon,
|
|
24
29
|
MicrophoneIcon,
|
|
25
30
|
SpeakerWaveIcon,
|
|
26
|
-
StopIcon
|
|
31
|
+
StopIcon,
|
|
27
32
|
} from '@heroicons/react/24/outline';
|
|
28
33
|
|
|
29
34
|
// Add type for webkitSpeechRecognition
|
|
30
35
|
declare global {
|
|
31
36
|
interface Window {
|
|
32
|
-
webkitSpeechRecognition
|
|
37
|
+
webkitSpeechRecognition?: typeof SpeechRecognition;
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Token Optimization: Reduce token count for LLM API calls
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
const MAX_HISTORY_MESSAGES = 20; // Sliding window size
|
|
46
|
+
const MAX_TOOL_RESULT_LENGTH = 2000; // Truncate large tool results
|
|
47
|
+
const MAX_CONTENT_LENGTH = 4000; // Truncate very long content
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optimize messages for LLM to reduce token count while preserving context
|
|
51
|
+
* - Strips widget data (result) from toolCalls (only needed for UI)
|
|
52
|
+
* - Truncates large tool results
|
|
53
|
+
* - Applies sliding window for message history
|
|
54
|
+
* - Removes file data from old messages
|
|
55
|
+
*/
|
|
56
|
+
function optimizeMessagesForLLM(messages: ChatMessage[]): ChatMessage[] {
|
|
57
|
+
// Apply sliding window - keep last N messages
|
|
58
|
+
// But always keep system messages and the most recent exchange
|
|
59
|
+
const recentMessages = messages.length > MAX_HISTORY_MESSAGES
|
|
60
|
+
? messages.slice(-MAX_HISTORY_MESSAGES)
|
|
61
|
+
: messages;
|
|
62
|
+
|
|
63
|
+
return recentMessages.map((msg, idx) => {
|
|
64
|
+
const isLastMessage = idx === recentMessages.length - 1;
|
|
65
|
+
|
|
66
|
+
const cleaned: ChatMessage = {
|
|
67
|
+
role: msg.role,
|
|
68
|
+
content: msg.content || '',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Strip result from toolCalls - LLM already gets results via tool role messages
|
|
72
|
+
// The result is only needed for UI widget rendering, not for LLM context
|
|
73
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
74
|
+
cleaned.toolCalls = msg.toolCalls.map(tc => ({
|
|
75
|
+
id: tc.id,
|
|
76
|
+
name: tc.name,
|
|
77
|
+
arguments: tc.arguments,
|
|
78
|
+
// result intentionally omitted - saves significant tokens!
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (msg.toolCallId) {
|
|
83
|
+
cleaned.toolCallId = msg.toolCallId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Truncate large tool results to save tokens
|
|
87
|
+
if (msg.role === 'tool' && cleaned.content.length > MAX_TOOL_RESULT_LENGTH) {
|
|
88
|
+
// Try to parse and summarize JSON results
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(cleaned.content);
|
|
91
|
+
if (Array.isArray(parsed)) {
|
|
92
|
+
// For arrays (like flight results), keep first few items
|
|
93
|
+
const truncated = parsed.slice(0, 3);
|
|
94
|
+
cleaned.content = JSON.stringify(truncated) +
|
|
95
|
+
`\n[... ${parsed.length - 3} more items truncated for efficiency]`;
|
|
96
|
+
} else if (typeof parsed === 'object') {
|
|
97
|
+
// For objects, stringify with limit
|
|
98
|
+
cleaned.content = cleaned.content.substring(0, MAX_TOOL_RESULT_LENGTH) +
|
|
99
|
+
'\n[Result truncated for context efficiency]';
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Not JSON, just truncate
|
|
103
|
+
cleaned.content = cleaned.content.substring(0, MAX_TOOL_RESULT_LENGTH) +
|
|
104
|
+
'\n[Result truncated for context efficiency]';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Truncate very long assistant/user content (rare but possible)
|
|
109
|
+
if ((msg.role === 'assistant' || msg.role === 'user') &&
|
|
110
|
+
cleaned.content.length > MAX_CONTENT_LENGTH) {
|
|
111
|
+
cleaned.content = cleaned.content.substring(0, MAX_CONTENT_LENGTH) +
|
|
112
|
+
'\n[Content truncated]';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Only include file data for the CURRENT message, not history
|
|
116
|
+
// Images are expensive (base64) and LLM already saw them
|
|
117
|
+
if (msg.file && isLastMessage) {
|
|
118
|
+
cleaned.file = msg.file;
|
|
119
|
+
}
|
|
120
|
+
// Old messages with files: just note that an image was attached
|
|
121
|
+
else if (msg.file && !isLastMessage) {
|
|
122
|
+
cleaned.content = cleaned.content || '[Image was attached]';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return cleaned;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
36
129
|
export default function ChatPage() {
|
|
37
130
|
const {
|
|
38
131
|
chatMessages,
|
|
@@ -48,6 +141,9 @@ export default function ChatPage() {
|
|
|
48
141
|
setElevenLabsApiKey
|
|
49
142
|
} = useStudioStore();
|
|
50
143
|
|
|
144
|
+
// Ops state
|
|
145
|
+
const { isOpsViewOpen, toggleOpsView, clearSession: clearOpsSessionStore } = useOpsStore();
|
|
146
|
+
|
|
51
147
|
// ... (existing helper methods)
|
|
52
148
|
const getAuthTokens = () => {
|
|
53
149
|
const state = useStudioStore.getState();
|
|
@@ -64,7 +160,11 @@ export default function ChatPage() {
|
|
|
64
160
|
const [prompts, setPrompts] = useState<Prompt[]>([]);
|
|
65
161
|
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
|
66
162
|
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
|
|
67
|
-
const [fullscreenWidget, setFullscreenWidget] = useState<{ uri: string, data:
|
|
163
|
+
const [fullscreenWidget, setFullscreenWidget] = useState<{ uri: string, data: unknown } | null>(null);
|
|
164
|
+
|
|
165
|
+
// API key inputs - start empty to avoid hydration mismatch
|
|
166
|
+
const [openaiApiKeyInput, setOpenaiApiKeyInput] = useState('');
|
|
167
|
+
const [geminiApiKeyInput, setGeminiApiKeyInput] = useState('');
|
|
68
168
|
|
|
69
169
|
// Language presets for quick selection
|
|
70
170
|
const LANG_PRESETS: Record<string, { model: string; voice: string; input: string; name: string; greeting: string }> = {
|
|
@@ -86,31 +186,31 @@ export default function ChatPage() {
|
|
|
86
186
|
const [voiceDisplayMode, setVoiceDisplayMode] = useState<'voice-only' | 'voice-chat'>('voice-only');
|
|
87
187
|
const [showVoiceSettings, setShowVoiceSettings] = useState(false);
|
|
88
188
|
|
|
89
|
-
// Voice Configuration - load from localStorage
|
|
90
|
-
const [voiceModel, setVoiceModel] = useState(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
189
|
+
// Voice Configuration - use defaults initially, load from localStorage in useEffect
|
|
190
|
+
const [voiceModel, setVoiceModel] = useState('eleven_multilingual_v2');
|
|
191
|
+
const [outputLanguage, setOutputLanguage] = useState('en');
|
|
192
|
+
const [inputLanguage, setInputLanguage] = useState('en-US');
|
|
193
|
+
const [voiceId, setVoiceId] = useState('21m00Tcm4TlvDq8ikWAM');
|
|
194
|
+
|
|
195
|
+
// Load settings from localStorage after mount (avoids hydration mismatch)
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
// Voice settings
|
|
198
|
+
const savedVoiceModel = localStorage.getItem('voice_model');
|
|
199
|
+
const savedOutputLang = localStorage.getItem('output_language');
|
|
200
|
+
const savedInputLang = localStorage.getItem('input_language');
|
|
201
|
+
const savedVoiceId = localStorage.getItem('voice_id');
|
|
202
|
+
|
|
203
|
+
if (savedVoiceModel) setVoiceModel(savedVoiceModel);
|
|
204
|
+
if (savedOutputLang) setOutputLanguage(savedOutputLang);
|
|
205
|
+
if (savedInputLang) setInputLanguage(savedInputLang);
|
|
206
|
+
if (savedVoiceId) setVoiceId(savedVoiceId);
|
|
207
|
+
|
|
208
|
+
// API keys
|
|
209
|
+
const savedOpenaiKey = localStorage.getItem('openai_api_key');
|
|
210
|
+
const savedGeminiKey = localStorage.getItem('gemini_api_key');
|
|
211
|
+
if (savedOpenaiKey) setOpenaiApiKeyInput(savedOpenaiKey);
|
|
212
|
+
if (savedGeminiKey) setGeminiApiKeyInput(savedGeminiKey);
|
|
213
|
+
}, []);
|
|
114
214
|
|
|
115
215
|
// Dynamic API data
|
|
116
216
|
interface ElevenLabsModel {
|
|
@@ -136,6 +236,24 @@ export default function ChatPage() {
|
|
|
136
236
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
137
237
|
const initialToolExecuted = useRef(false);
|
|
138
238
|
|
|
239
|
+
// Load tools and prompts on mount
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
loadTools();
|
|
242
|
+
loadPrompts();
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
// Auto-execute initial tool when tools are loaded
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
if (tools.length > 0 && !initialToolExecuted.current) {
|
|
248
|
+
checkAndRunInitialTool();
|
|
249
|
+
}
|
|
250
|
+
}, [tools]);
|
|
251
|
+
|
|
252
|
+
// Scroll to bottom when messages change
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
255
|
+
}, [chatMessages]);
|
|
256
|
+
|
|
139
257
|
// Fetch ElevenLabs models when settings opens
|
|
140
258
|
useEffect(() => {
|
|
141
259
|
if ((!showVoiceSettings && !showSettings) || !elevenLabsApiKey) return;
|
|
@@ -189,7 +307,13 @@ export default function ChatPage() {
|
|
|
189
307
|
let sharedVoices: ElevenLabsVoice[] = [];
|
|
190
308
|
if (sharedVoicesRes.ok) {
|
|
191
309
|
const data = await sharedVoicesRes.json();
|
|
192
|
-
|
|
310
|
+
interface SharedVoiceData {
|
|
311
|
+
voice_id: string;
|
|
312
|
+
name: string;
|
|
313
|
+
accent?: string;
|
|
314
|
+
language?: string;
|
|
315
|
+
}
|
|
316
|
+
sharedVoices = (data.voices || []).map((v: SharedVoiceData) => ({
|
|
193
317
|
voice_id: v.voice_id,
|
|
194
318
|
name: v.name,
|
|
195
319
|
labels: { accent: v.accent || v.language },
|
|
@@ -230,6 +354,119 @@ export default function ChatPage() {
|
|
|
230
354
|
}
|
|
231
355
|
}, [chatMessages, voiceModeEnabled, voiceOverlayOpen, elevenLabsApiKey]);
|
|
232
356
|
|
|
357
|
+
// Listen for widget-to-MCP tool calls
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
const handleWidgetToolCall = async (event: CustomEvent<{ toolName: string; toolArgs: Record<string, unknown> }>) => {
|
|
360
|
+
const { toolName, toolArgs } = event.detail;
|
|
361
|
+
console.log('🔧 Widget tool call received:', toolName, toolArgs);
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
setLoading(true);
|
|
365
|
+
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
366
|
+
const effectiveToken = jwtToken || useStudioStore.getState().oauthState?.currentToken;
|
|
367
|
+
|
|
368
|
+
// Track ops: Start tool call from widget interaction
|
|
369
|
+
const toolCallId = `call_${Date.now()}`;
|
|
370
|
+
opsTracker.startToolCall(toolCallId, toolName, toolArgs || {});
|
|
371
|
+
const toolStartTime = Date.now();
|
|
372
|
+
|
|
373
|
+
// Call the tool via API
|
|
374
|
+
const rawResult = await api.callTool(
|
|
375
|
+
toolName,
|
|
376
|
+
toolArgs || {},
|
|
377
|
+
effectiveToken,
|
|
378
|
+
mcpApiKey || undefined
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
console.log('✅ Widget tool call raw result:', rawResult);
|
|
382
|
+
|
|
383
|
+
// Parse the MCP result format: { content: [{ type: "text", text: "..." }] }
|
|
384
|
+
let parsedResult = rawResult;
|
|
385
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
386
|
+
const mcpResult = rawResult as any;
|
|
387
|
+
if (mcpResult?.content?.[0]?.text) {
|
|
388
|
+
try {
|
|
389
|
+
parsedResult = JSON.parse(mcpResult.content[0].text);
|
|
390
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
391
|
+
if (parsedResult && typeof parsedResult === 'object' && 'success' in parsedResult && 'data' in parsedResult) {
|
|
392
|
+
parsedResult = (parsedResult as { success: boolean; data: unknown }).data;
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
parsedResult = { content: mcpResult.content[0].text };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('✅ Widget tool call parsed result:', parsedResult);
|
|
400
|
+
|
|
401
|
+
// Track ops: Complete tool call with result
|
|
402
|
+
const resultNodeId = opsTracker.completeToolCall(toolCallId, toolName, parsedResult);
|
|
403
|
+
|
|
404
|
+
// Track ops: Widget render
|
|
405
|
+
const toolDef = tools.find(t => t.name === toolName);
|
|
406
|
+
const widgetUri = toolDef?.widget?.route || toolDef?.outputTemplate || toolDef?._meta?.['openai/outputTemplate'];
|
|
407
|
+
if (widgetUri && parsedResult && resultNodeId) {
|
|
408
|
+
opsTracker.trackWidget(widgetUri, toolName, parsedResult, resultNodeId);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Add assistant message with tool call info
|
|
412
|
+
const assistantMsg: ChatMessage = {
|
|
413
|
+
role: 'assistant',
|
|
414
|
+
content: ``,
|
|
415
|
+
toolCalls: [{
|
|
416
|
+
id: toolCallId,
|
|
417
|
+
name: toolName,
|
|
418
|
+
arguments: toolArgs || {},
|
|
419
|
+
result: parsedResult // Attach parsed result for widget rendering
|
|
420
|
+
}]
|
|
421
|
+
};
|
|
422
|
+
addChatMessage(assistantMsg);
|
|
423
|
+
|
|
424
|
+
// Add tool result message
|
|
425
|
+
const toolResultMsg: ChatMessage = {
|
|
426
|
+
role: 'tool',
|
|
427
|
+
content: JSON.stringify(parsedResult),
|
|
428
|
+
toolCallId: toolCallId
|
|
429
|
+
};
|
|
430
|
+
addChatMessage(toolResultMsg);
|
|
431
|
+
|
|
432
|
+
// Continue conversation to get LLM response about the tool result (like nitrochat)
|
|
433
|
+
const apiKey = localStorage.getItem(`${currentProvider}_api_key`);
|
|
434
|
+
if (apiKey && apiKey !== '••••••••') {
|
|
435
|
+
// Get current messages and add instruction to prevent tool chaining
|
|
436
|
+
const currentMessages = useStudioStore.getState().chatMessages;
|
|
437
|
+
|
|
438
|
+
// Add a system-style instruction (as user message) to prevent the LLM from calling more tools
|
|
439
|
+
// This is hidden from the user but guides the LLM to just summarize
|
|
440
|
+
const messagesForLLM: ChatMessage[] = [
|
|
441
|
+
...currentMessages,
|
|
442
|
+
{
|
|
443
|
+
role: 'user',
|
|
444
|
+
content: `[INSTRUCTION: The user clicked on a widget item which called the "${toolName}" tool. Please summarize the result above in a helpful way. IMPORTANT: Do NOT call any additional tools - just provide a text summary of what was retrieved. The user only wanted to see details for this specific item.]`
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
await continueChatWithToolResults(apiKey, messagesForLLM, true);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error('❌ Widget tool call failed:', error);
|
|
453
|
+
// Track ops: Tool call error
|
|
454
|
+
opsTracker.errorLLMCall();
|
|
455
|
+
addChatMessage({
|
|
456
|
+
role: 'assistant',
|
|
457
|
+
content: `Failed to execute tool ${toolName}: ${error instanceof Error ? error.message : String(error)}`
|
|
458
|
+
});
|
|
459
|
+
} finally {
|
|
460
|
+
setLoading(false);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
window.addEventListener('widget-tool-call', handleWidgetToolCall as EventListener);
|
|
465
|
+
return () => {
|
|
466
|
+
window.removeEventListener('widget-tool-call', handleWidgetToolCall as EventListener);
|
|
467
|
+
};
|
|
468
|
+
}, [addChatMessage, currentProvider]);
|
|
469
|
+
|
|
233
470
|
// Convert markdown content to voice-friendly, conversational text
|
|
234
471
|
// Optimized for minimal TTS token usage
|
|
235
472
|
const convertToVoiceFriendlyText = (text: string): string => {
|
|
@@ -423,36 +660,65 @@ export default function ChatPage() {
|
|
|
423
660
|
initialToolExecuted.current = true;
|
|
424
661
|
console.log('🚀 Auto-executing initial tool:', initialTool.name);
|
|
425
662
|
|
|
426
|
-
// Initial message
|
|
427
|
-
const autoMsg: ChatMessage = {
|
|
428
|
-
role: 'user',
|
|
429
|
-
content: `(Auto) Executing initial tool: ${initialTool.name}`,
|
|
430
|
-
};
|
|
431
|
-
addChatMessage(autoMsg);
|
|
432
663
|
setLoading(true);
|
|
433
664
|
|
|
665
|
+
// Track ops: Start a new turn for initial tool
|
|
666
|
+
opsTracker.startTurn(`[Initial] ${initialTool.name}`);
|
|
667
|
+
|
|
434
668
|
try {
|
|
435
669
|
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
436
670
|
const effectiveToken = jwtToken || useStudioStore.getState().oauthState?.currentToken;
|
|
437
671
|
|
|
672
|
+
// Track ops: Start tool call
|
|
673
|
+
const toolCallId = `call_${Date.now()}_init`;
|
|
674
|
+
opsTracker.startToolCall(toolCallId, initialTool.name, {});
|
|
675
|
+
|
|
438
676
|
// Call the tool
|
|
439
|
-
const
|
|
677
|
+
const rawResult = await api.callTool(
|
|
440
678
|
initialTool.name,
|
|
441
679
|
{},
|
|
442
680
|
effectiveToken,
|
|
443
681
|
mcpApiKey || undefined
|
|
444
682
|
);
|
|
445
683
|
|
|
446
|
-
|
|
447
|
-
|
|
684
|
+
console.log('✅ Initial tool raw result:', rawResult);
|
|
685
|
+
|
|
686
|
+
// Parse the MCP result format: { content: [{ type: "text", text: "..." }] }
|
|
687
|
+
let parsedResult = rawResult;
|
|
688
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
689
|
+
const mcpResult = rawResult as any;
|
|
690
|
+
if (mcpResult?.content?.[0]?.text) {
|
|
691
|
+
try {
|
|
692
|
+
parsedResult = JSON.parse(mcpResult.content[0].text);
|
|
693
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
694
|
+
if (parsedResult && typeof parsedResult === 'object' && 'success' in parsedResult && 'data' in parsedResult) {
|
|
695
|
+
parsedResult = (parsedResult as { success: boolean; data: unknown }).data;
|
|
696
|
+
}
|
|
697
|
+
} catch {
|
|
698
|
+
parsedResult = { content: mcpResult.content[0].text };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
console.log('✅ Initial tool parsed result:', parsedResult);
|
|
703
|
+
|
|
704
|
+
// Track ops: Complete tool call with result
|
|
705
|
+
const resultNodeId = opsTracker.completeToolCall(toolCallId, initialTool.name, parsedResult);
|
|
706
|
+
|
|
707
|
+
// Track ops: Widget render
|
|
708
|
+
const widgetUri = initialTool?.widget?.route || initialTool?.outputTemplate || initialTool?._meta?.['openai/outputTemplate'];
|
|
709
|
+
if (widgetUri && parsedResult && resultNodeId) {
|
|
710
|
+
opsTracker.trackWidget(widgetUri, initialTool.name, parsedResult, resultNodeId);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Add assistant message with tool call info (widget will render from this)
|
|
448
714
|
const assistantMsg: ChatMessage = {
|
|
449
715
|
role: 'assistant',
|
|
450
|
-
content:
|
|
716
|
+
content: '',
|
|
451
717
|
toolCalls: [{
|
|
452
718
|
id: toolCallId,
|
|
453
719
|
name: initialTool.name,
|
|
454
720
|
arguments: {},
|
|
455
|
-
result // Attach result
|
|
721
|
+
result: parsedResult // Attach parsed result for widget rendering
|
|
456
722
|
}]
|
|
457
723
|
};
|
|
458
724
|
addChatMessage(assistantMsg);
|
|
@@ -460,13 +726,18 @@ export default function ChatPage() {
|
|
|
460
726
|
// Add tool result message
|
|
461
727
|
const toolResultMsg: ChatMessage = {
|
|
462
728
|
role: 'tool',
|
|
463
|
-
content: JSON.stringify(
|
|
729
|
+
content: JSON.stringify(parsedResult),
|
|
464
730
|
toolCallId: toolCallId
|
|
465
731
|
};
|
|
466
732
|
addChatMessage(toolResultMsg);
|
|
467
733
|
|
|
734
|
+
// Note: Like nitrochat, we don't continue chat automatically here
|
|
735
|
+
// We just show the result (widget) as the starting state
|
|
736
|
+
|
|
468
737
|
} catch (error) {
|
|
469
|
-
console.error('Initial tool execution failed:', error);
|
|
738
|
+
console.error('❌ Initial tool execution failed:', error);
|
|
739
|
+
// Track ops: Error
|
|
740
|
+
opsTracker.errorLLMCall();
|
|
470
741
|
addChatMessage({
|
|
471
742
|
role: 'assistant',
|
|
472
743
|
content: `Failed to execute initial tool ${initialTool.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -506,11 +777,15 @@ export default function ChatPage() {
|
|
|
506
777
|
// Add the prompt result as an assistant message
|
|
507
778
|
if (result.messages && result.messages.length > 0) {
|
|
508
779
|
// Combine all prompt messages into one assistant message
|
|
509
|
-
|
|
510
|
-
|
|
780
|
+
interface PromptMessageResult {
|
|
781
|
+
role: string;
|
|
782
|
+
content: string | { text?: string } | unknown;
|
|
783
|
+
}
|
|
784
|
+
const combinedContent = (result.messages as PromptMessageResult[])
|
|
785
|
+
.map((msg) => {
|
|
511
786
|
const content = typeof msg.content === 'string'
|
|
512
787
|
? msg.content
|
|
513
|
-
: msg.content?.text || JSON.stringify(msg.content);
|
|
788
|
+
: (msg.content as { text?: string })?.text || JSON.stringify(msg.content);
|
|
514
789
|
return `[${msg.role.toUpperCase()}]\n${content}`;
|
|
515
790
|
})
|
|
516
791
|
.join('\n\n');
|
|
@@ -584,31 +859,14 @@ export default function ChatPage() {
|
|
|
584
859
|
setCurrentFile(null);
|
|
585
860
|
setLoading(true);
|
|
586
861
|
|
|
862
|
+
// Track ops: Start a new turn
|
|
863
|
+
opsTracker.startTurn(messageText, !!currentFile);
|
|
864
|
+
|
|
587
865
|
try {
|
|
588
866
|
const messagesToSend = [...chatMessages, userMessage];
|
|
589
867
|
|
|
590
|
-
//
|
|
591
|
-
const cleanedMessages = messagesToSend
|
|
592
|
-
const cleaned: any = {
|
|
593
|
-
role: msg.role,
|
|
594
|
-
content: msg.content || '',
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
598
|
-
cleaned.toolCalls = msg.toolCalls;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (msg.toolCallId) {
|
|
602
|
-
cleaned.toolCallId = msg.toolCallId;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Skip image property for now (not supported by OpenAI chat completions)
|
|
606
|
-
if (msg.file) {
|
|
607
|
-
cleaned.file = msg.file;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
return cleaned;
|
|
611
|
-
});
|
|
868
|
+
// Optimize messages to reduce token count
|
|
869
|
+
const cleanedMessages = optimizeMessagesForLLM(messagesToSend);
|
|
612
870
|
|
|
613
871
|
// Get fresh auth tokens from store
|
|
614
872
|
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
@@ -633,6 +891,10 @@ export default function ChatPage() {
|
|
|
633
891
|
console.log('Original messages:', messagesToSend);
|
|
634
892
|
console.log('Voice mode:', voiceModeEnabled, 'Output language:', outputLanguage);
|
|
635
893
|
|
|
894
|
+
// Track ops: LLM call start
|
|
895
|
+
const llmModel = currentProvider === 'gemini' ? 'gemini-1.5-pro' : 'gpt-4o';
|
|
896
|
+
opsTracker.startLLMCall(currentProvider as LLMProvider, llmModel);
|
|
897
|
+
|
|
636
898
|
const response = await api.chat({
|
|
637
899
|
provider: currentProvider,
|
|
638
900
|
messages: messagesForApi,
|
|
@@ -641,14 +903,23 @@ export default function ChatPage() {
|
|
|
641
903
|
mcpApiKey: mcpApiKey || undefined, // MCP server API key
|
|
642
904
|
});
|
|
643
905
|
|
|
906
|
+
// Track ops: LLM call complete (estimate tokens based on content length)
|
|
907
|
+
const inputTokenEstimate = Math.ceil(JSON.stringify(messagesForApi).length / 4);
|
|
908
|
+
const outputTokenEstimate = Math.ceil((response.message?.content?.length || 0) / 4);
|
|
909
|
+
opsTracker.completeLLMCall(inputTokenEstimate, outputTokenEstimate);
|
|
910
|
+
|
|
644
911
|
// Handle tool calls FIRST (before adding the message)
|
|
645
912
|
if (response.toolCalls && response.toolResults) {
|
|
646
|
-
//
|
|
913
|
+
// Track ops: Process each tool call
|
|
647
914
|
const toolCallsWithResults = response.toolCalls.map((tc: ToolCall, i: number) => {
|
|
648
915
|
const toolResult = response.toolResults[i];
|
|
649
916
|
|
|
917
|
+
// Track ops: Tool call start
|
|
918
|
+
opsTracker.startToolCall(tc.id, tc.name, tc.arguments as Record<string, unknown>);
|
|
919
|
+
|
|
650
920
|
// Parse the result content
|
|
651
921
|
let parsedResult;
|
|
922
|
+
let hasError = false;
|
|
652
923
|
if (toolResult.content) {
|
|
653
924
|
try {
|
|
654
925
|
parsedResult = JSON.parse(toolResult.content);
|
|
@@ -662,6 +933,16 @@ export default function ChatPage() {
|
|
|
662
933
|
}
|
|
663
934
|
}
|
|
664
935
|
|
|
936
|
+
// Track ops: Tool call complete with result
|
|
937
|
+
const resultNodeId = opsTracker.completeToolCall(tc.id, tc.name, parsedResult, hasError ? 'Error processing result' : undefined);
|
|
938
|
+
|
|
939
|
+
// Track ops: Widget render if tool has a widget
|
|
940
|
+
const toolDef = tools.find(t => t.name === tc.name);
|
|
941
|
+
const widgetUri = toolDef?.widget?.route || toolDef?.outputTemplate || toolDef?._meta?.['openai/outputTemplate'];
|
|
942
|
+
if (widgetUri && parsedResult && resultNodeId) {
|
|
943
|
+
opsTracker.trackWidget(widgetUri, tc.name, parsedResult, resultNodeId);
|
|
944
|
+
}
|
|
945
|
+
|
|
665
946
|
return {
|
|
666
947
|
...tc,
|
|
667
948
|
result: parsedResult,
|
|
@@ -714,6 +995,12 @@ export default function ChatPage() {
|
|
|
714
995
|
// No tool calls, just add the message
|
|
715
996
|
if (response.message) {
|
|
716
997
|
addChatMessage(response.message);
|
|
998
|
+
|
|
999
|
+
// Track ops: Final response
|
|
1000
|
+
opsTracker.trackResponse(
|
|
1001
|
+
response.message.content || '',
|
|
1002
|
+
outputTokenEstimate
|
|
1003
|
+
);
|
|
717
1004
|
}
|
|
718
1005
|
}
|
|
719
1006
|
|
|
@@ -721,6 +1008,10 @@ export default function ChatPage() {
|
|
|
721
1008
|
setLoading(false);
|
|
722
1009
|
} catch (error) {
|
|
723
1010
|
console.error('Chat error:', error);
|
|
1011
|
+
|
|
1012
|
+
// Track ops: LLM call error
|
|
1013
|
+
opsTracker.errorLLMCall();
|
|
1014
|
+
|
|
724
1015
|
addChatMessage({
|
|
725
1016
|
role: 'assistant',
|
|
726
1017
|
content: 'Sorry, I encountered an error. Please try again.',
|
|
@@ -729,7 +1020,7 @@ export default function ChatPage() {
|
|
|
729
1020
|
}
|
|
730
1021
|
};
|
|
731
1022
|
|
|
732
|
-
const continueChatWithToolResults = async (apiKey: string, messages?: ChatMessage[]) => {
|
|
1023
|
+
const continueChatWithToolResults = async (apiKey: string, messages?: ChatMessage[], isFromWidget: boolean = false) => {
|
|
733
1024
|
try {
|
|
734
1025
|
// Use provided messages or fall back to store (for recursive calls)
|
|
735
1026
|
const messagesToUse = messages || chatMessages;
|
|
@@ -737,26 +1028,15 @@ export default function ChatPage() {
|
|
|
737
1028
|
// Get fresh auth tokens from store (token may have been updated by login)
|
|
738
1029
|
const { jwtToken, mcpApiKey } = getAuthTokens();
|
|
739
1030
|
|
|
740
|
-
//
|
|
741
|
-
const cleanedMessages = messagesToUse
|
|
742
|
-
const cleaned: any = {
|
|
743
|
-
role: msg.role,
|
|
744
|
-
content: msg.content || '',
|
|
745
|
-
};
|
|
1031
|
+
// Optimize messages to reduce token count
|
|
1032
|
+
const cleanedMessages = optimizeMessagesForLLM(messagesToUse);
|
|
746
1033
|
|
|
747
|
-
|
|
748
|
-
cleaned.toolCalls = msg.toolCalls;
|
|
749
|
-
}
|
|
1034
|
+
console.log('Continue with optimized messages:', cleanedMessages.length, 'messages');
|
|
750
1035
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return cleaned;
|
|
756
|
-
});
|
|
757
|
-
|
|
758
|
-
console.log('Continue with cleaned messages:', JSON.stringify(cleanedMessages));
|
|
759
|
-
console.log('Continue auth tokens:', { hasJwtToken: !!jwtToken, hasMcpApiKey: !!mcpApiKey });
|
|
1036
|
+
// Track ops: LLM call start for continuation
|
|
1037
|
+
const llmModel = currentProvider === 'gemini' ? 'gemini-1.5-pro' : 'gpt-4o';
|
|
1038
|
+
opsTracker.startLLMCall(currentProvider as LLMProvider, llmModel);
|
|
1039
|
+
const llmStartTime = Date.now();
|
|
760
1040
|
|
|
761
1041
|
const response = await api.chat({
|
|
762
1042
|
provider: currentProvider,
|
|
@@ -766,12 +1046,58 @@ export default function ChatPage() {
|
|
|
766
1046
|
mcpApiKey: mcpApiKey || undefined, // MCP server API key
|
|
767
1047
|
});
|
|
768
1048
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1049
|
+
// Track ops: LLM call complete with token estimates
|
|
1050
|
+
const inputTokenEstimate = Math.ceil(JSON.stringify(cleanedMessages).length / 4);
|
|
1051
|
+
const outputTokenEstimate = Math.ceil((response.message?.content?.length || 0) / 4);
|
|
1052
|
+
opsTracker.completeLLMCall(inputTokenEstimate, outputTokenEstimate);
|
|
772
1053
|
|
|
773
|
-
//
|
|
1054
|
+
// Handle tool calls - attach results before adding message (same as handleSend)
|
|
774
1055
|
if (response.toolCalls && response.toolResults) {
|
|
1056
|
+
// Track ops: Process each tool call
|
|
1057
|
+
const toolCallsWithResults = response.toolCalls.map((tc: ToolCall, i: number) => {
|
|
1058
|
+
const toolResult = response.toolResults[i];
|
|
1059
|
+
|
|
1060
|
+
// Track ops: Tool call start
|
|
1061
|
+
opsTracker.startToolCall(tc.id, tc.name, tc.arguments as Record<string, unknown>);
|
|
1062
|
+
|
|
1063
|
+
// Parse the result content
|
|
1064
|
+
let parsedResult;
|
|
1065
|
+
if (toolResult.content) {
|
|
1066
|
+
try {
|
|
1067
|
+
parsedResult = JSON.parse(toolResult.content);
|
|
1068
|
+
|
|
1069
|
+
// Unwrap if response was wrapped by TransformInterceptor
|
|
1070
|
+
if (parsedResult.success !== undefined && parsedResult.data !== undefined) {
|
|
1071
|
+
parsedResult = parsedResult.data;
|
|
1072
|
+
}
|
|
1073
|
+
} catch {
|
|
1074
|
+
parsedResult = { content: toolResult.content };
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Track ops: Tool call complete with result
|
|
1079
|
+
const resultNodeId = opsTracker.completeToolCall(tc.id, tc.name, parsedResult);
|
|
1080
|
+
|
|
1081
|
+
// Track ops: Widget render if tool has a widget
|
|
1082
|
+
const toolDef = tools.find(t => t.name === tc.name);
|
|
1083
|
+
const widgetUri = toolDef?.widget?.route || toolDef?.outputTemplate || toolDef?._meta?.['openai/outputTemplate'];
|
|
1084
|
+
if (widgetUri && parsedResult && resultNodeId) {
|
|
1085
|
+
opsTracker.trackWidget(widgetUri, tc.name, parsedResult, resultNodeId);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return {
|
|
1089
|
+
...tc,
|
|
1090
|
+
result: parsedResult,
|
|
1091
|
+
};
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
// Add assistant message with tool calls (with results attached)
|
|
1095
|
+
if (response.message) {
|
|
1096
|
+
response.message.toolCalls = toolCallsWithResults;
|
|
1097
|
+
addChatMessage(response.message);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Add tool result messages
|
|
775
1101
|
const newToolResults: ChatMessage[] = [];
|
|
776
1102
|
for (const result of response.toolResults) {
|
|
777
1103
|
addChatMessage(result);
|
|
@@ -784,16 +1110,28 @@ export default function ChatPage() {
|
|
|
784
1110
|
response.message!,
|
|
785
1111
|
...newToolResults,
|
|
786
1112
|
];
|
|
787
|
-
await continueChatWithToolResults(apiKey, nextMessages);
|
|
1113
|
+
await continueChatWithToolResults(apiKey, nextMessages, false);
|
|
1114
|
+
} else {
|
|
1115
|
+
// No tool calls, just add the message
|
|
1116
|
+
if (response.message) {
|
|
1117
|
+
addChatMessage(response.message);
|
|
1118
|
+
|
|
1119
|
+
// Track ops: Final response
|
|
1120
|
+
opsTracker.trackResponse(
|
|
1121
|
+
response.message.content || '',
|
|
1122
|
+
outputTokenEstimate
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
788
1125
|
}
|
|
789
1126
|
} catch (error) {
|
|
790
1127
|
console.error('Continuation error:', error);
|
|
1128
|
+
// Track ops: Error
|
|
1129
|
+
opsTracker.errorLLMCall();
|
|
791
1130
|
}
|
|
792
1131
|
};
|
|
793
1132
|
|
|
794
1133
|
const saveApiKey = (provider: 'openai' | 'gemini') => {
|
|
795
|
-
const
|
|
796
|
-
const key = input?.value.trim();
|
|
1134
|
+
const key = provider === 'openai' ? openaiApiKeyInput.trim() : geminiApiKeyInput.trim();
|
|
797
1135
|
|
|
798
1136
|
if (!key || key === '••••••••') {
|
|
799
1137
|
alert('Please enter a valid API key');
|
|
@@ -801,7 +1139,12 @@ export default function ChatPage() {
|
|
|
801
1139
|
}
|
|
802
1140
|
|
|
803
1141
|
localStorage.setItem(`${provider}_api_key`, key);
|
|
804
|
-
|
|
1142
|
+
// Show masked value after save
|
|
1143
|
+
if (provider === 'openai') {
|
|
1144
|
+
setOpenaiApiKeyInput('••••••••');
|
|
1145
|
+
} else {
|
|
1146
|
+
setGeminiApiKeyInput('••••••••');
|
|
1147
|
+
}
|
|
805
1148
|
alert(`${provider === 'openai' ? 'OpenAI' : 'Gemini'} API key saved`);
|
|
806
1149
|
};
|
|
807
1150
|
|
|
@@ -856,6 +1199,20 @@ export default function ChatPage() {
|
|
|
856
1199
|
)}
|
|
857
1200
|
|
|
858
1201
|
|
|
1202
|
+
{/* Ops View Toggle */}
|
|
1203
|
+
<button
|
|
1204
|
+
onClick={toggleOpsView}
|
|
1205
|
+
className={`h-8 w-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${isOpsViewOpen
|
|
1206
|
+
? 'bg-indigo-500/20 text-indigo-400 ring-1 ring-indigo-500/40'
|
|
1207
|
+
: 'bg-zinc-800/80 text-zinc-500 hover:bg-zinc-700/80 hover:text-zinc-300'
|
|
1208
|
+
}`}
|
|
1209
|
+
title="Operations"
|
|
1210
|
+
>
|
|
1211
|
+
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
1212
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
|
|
1213
|
+
</svg>
|
|
1214
|
+
</button>
|
|
1215
|
+
|
|
859
1216
|
<button
|
|
860
1217
|
onClick={() => setShowSettings(!showSettings)}
|
|
861
1218
|
className={`h-8 w-8 rounded-lg flex items-center justify-center transition-all flex-shrink-0 ${showSettings
|
|
@@ -867,7 +1224,10 @@ export default function ChatPage() {
|
|
|
867
1224
|
<Cog6ToothIcon className="h-4 w-4" />
|
|
868
1225
|
</button>
|
|
869
1226
|
<button
|
|
870
|
-
onClick={
|
|
1227
|
+
onClick={() => {
|
|
1228
|
+
clearChat();
|
|
1229
|
+
clearOpsSessionStore();
|
|
1230
|
+
}}
|
|
871
1231
|
className="h-8 w-8 rounded-lg flex items-center justify-center bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all flex-shrink-0"
|
|
872
1232
|
title="Clear chat"
|
|
873
1233
|
>
|
|
@@ -974,7 +1334,8 @@ export default function ChatPage() {
|
|
|
974
1334
|
type="password"
|
|
975
1335
|
className="input flex-1 text-sm bg-background/50"
|
|
976
1336
|
placeholder="sk-proj-..."
|
|
977
|
-
|
|
1337
|
+
value={openaiApiKeyInput}
|
|
1338
|
+
onChange={(e) => setOpenaiApiKeyInput(e.target.value)}
|
|
978
1339
|
/>
|
|
979
1340
|
<button onClick={() => saveApiKey('openai')} className="btn btn-primary btn-sm px-4">
|
|
980
1341
|
Save
|
|
@@ -1006,7 +1367,8 @@ export default function ChatPage() {
|
|
|
1006
1367
|
type="password"
|
|
1007
1368
|
className="input flex-1 text-sm bg-background/50"
|
|
1008
1369
|
placeholder="AIza..."
|
|
1009
|
-
|
|
1370
|
+
value={geminiApiKeyInput}
|
|
1371
|
+
onChange={(e) => setGeminiApiKeyInput(e.target.value)}
|
|
1010
1372
|
/>
|
|
1011
1373
|
<button onClick={() => saveApiKey('gemini')} className="btn btn-primary btn-sm px-4">
|
|
1012
1374
|
Save
|
|
@@ -1176,9 +1538,12 @@ export default function ChatPage() {
|
|
|
1176
1538
|
</div>
|
|
1177
1539
|
)}
|
|
1178
1540
|
|
|
1179
|
-
{/*
|
|
1180
|
-
<div className="flex-1 overflow-
|
|
1181
|
-
|
|
1541
|
+
{/* Main Content Area with optional Ops split view */}
|
|
1542
|
+
<div className="flex-1 flex overflow-hidden min-h-0">
|
|
1543
|
+
{/* Chat Messages Container */}
|
|
1544
|
+
<div className={`flex flex-col overflow-hidden transition-all duration-300 ${isOpsViewOpen ? 'w-3/5' : 'w-full'}`}>
|
|
1545
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
1546
|
+
<div className={`mx-auto px-4 py-6 space-y-6 ${isOpsViewOpen ? 'max-w-3xl' : 'max-w-5xl'}`}>
|
|
1182
1547
|
{chatMessages.length === 0 && !loading ? (
|
|
1183
1548
|
/* Welcome Screen */
|
|
1184
1549
|
<div className="flex flex-col items-center justify-center min-h-[calc(100vh-300px)] animate-fade-in">
|
|
@@ -1373,7 +1738,16 @@ export default function ChatPage() {
|
|
|
1373
1738
|
</>
|
|
1374
1739
|
)}
|
|
1375
1740
|
<div ref={messagesEndRef} />
|
|
1741
|
+
</div>
|
|
1742
|
+
</div>
|
|
1376
1743
|
</div>
|
|
1744
|
+
|
|
1745
|
+
{/* Ops Canvas Panel */}
|
|
1746
|
+
{isOpsViewOpen && (
|
|
1747
|
+
<div className="w-2/5 min-w-[320px] border-l border-zinc-800 flex flex-col overflow-hidden">
|
|
1748
|
+
<OpsCanvas className="flex-1" />
|
|
1749
|
+
</div>
|
|
1750
|
+
)}
|
|
1377
1751
|
</div>
|
|
1378
1752
|
|
|
1379
1753
|
{/* Sleek Professional Input Area */}
|
|
@@ -1878,9 +2252,11 @@ function ToolCallComponent({ toolCall, tools }: { toolCall: ToolCall; tools: Too
|
|
|
1878
2252
|
return (
|
|
1879
2253
|
<div className="relative group/widget">
|
|
1880
2254
|
{componentUri && widgetData && (
|
|
1881
|
-
<
|
|
1882
|
-
<
|
|
1883
|
-
|
|
2255
|
+
<WidgetErrorBoundary>
|
|
2256
|
+
<div className="rounded-lg overflow-hidden max-w-5xl" style={{ minHeight: '100px' }}>
|
|
2257
|
+
<WidgetRenderer uri={componentUri} data={widgetData} className="widget-in-chat" />
|
|
2258
|
+
</div>
|
|
2259
|
+
</WidgetErrorBoundary>
|
|
1884
2260
|
)}
|
|
1885
2261
|
<button
|
|
1886
2262
|
onClick={() => setShowArgs(!showArgs)}
|