flowquery 1.0.5 → 1.0.7

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.
Files changed (138) hide show
  1. package/README.md +74 -0
  2. package/dist/compute/runner.d.ts +1 -22
  3. package/dist/compute/runner.d.ts.map +1 -1
  4. package/dist/compute/runner.js.map +1 -1
  5. package/dist/extensibility.d.ts +35 -0
  6. package/dist/extensibility.d.ts.map +1 -0
  7. package/dist/extensibility.js +49 -0
  8. package/dist/extensibility.js.map +1 -0
  9. package/dist/flowquery.min.js +1 -1
  10. package/dist/index.browser.d.ts.map +1 -1
  11. package/dist/index.browser.js +0 -80
  12. package/dist/index.browser.js.map +1 -1
  13. package/dist/index.node.d.ts +3 -3
  14. package/dist/index.node.d.ts.map +1 -1
  15. package/dist/index.node.js +0 -80
  16. package/dist/index.node.js.map +1 -1
  17. package/dist/parsing/functions/avg.d.ts.map +1 -1
  18. package/dist/parsing/functions/avg.js +20 -2
  19. package/dist/parsing/functions/avg.js.map +1 -1
  20. package/dist/parsing/functions/collect.d.ts.map +1 -1
  21. package/dist/parsing/functions/collect.js +20 -2
  22. package/dist/parsing/functions/collect.js.map +1 -1
  23. package/dist/parsing/functions/function_factory.d.ts +26 -80
  24. package/dist/parsing/functions/function_factory.d.ts.map +1 -1
  25. package/dist/parsing/functions/function_factory.js +46 -168
  26. package/dist/parsing/functions/function_factory.js.map +1 -1
  27. package/dist/parsing/functions/function_metadata.d.ts +81 -20
  28. package/dist/parsing/functions/function_metadata.d.ts.map +1 -1
  29. package/dist/parsing/functions/function_metadata.js +154 -152
  30. package/dist/parsing/functions/function_metadata.js.map +1 -1
  31. package/dist/parsing/functions/functions.d.ts.map +1 -1
  32. package/dist/parsing/functions/functions.js +37 -2
  33. package/dist/parsing/functions/functions.js.map +1 -1
  34. package/dist/parsing/functions/join.d.ts.map +1 -1
  35. package/dist/parsing/functions/join.js +21 -2
  36. package/dist/parsing/functions/join.js.map +1 -1
  37. package/dist/parsing/functions/predicate_function.d.ts +1 -0
  38. package/dist/parsing/functions/predicate_function.d.ts.map +1 -1
  39. package/dist/parsing/functions/predicate_function.js +3 -0
  40. package/dist/parsing/functions/predicate_function.js.map +1 -1
  41. package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
  42. package/dist/parsing/functions/predicate_sum.js +23 -2
  43. package/dist/parsing/functions/predicate_sum.js.map +1 -1
  44. package/dist/parsing/functions/rand.d.ts.map +1 -1
  45. package/dist/parsing/functions/rand.js +18 -2
  46. package/dist/parsing/functions/rand.js.map +1 -1
  47. package/dist/parsing/functions/range.d.ts.map +1 -1
  48. package/dist/parsing/functions/range.js +21 -2
  49. package/dist/parsing/functions/range.js.map +1 -1
  50. package/dist/parsing/functions/replace.d.ts.map +1 -1
  51. package/dist/parsing/functions/replace.js +22 -2
  52. package/dist/parsing/functions/replace.js.map +1 -1
  53. package/dist/parsing/functions/round.d.ts.map +1 -1
  54. package/dist/parsing/functions/round.js +20 -2
  55. package/dist/parsing/functions/round.js.map +1 -1
  56. package/dist/parsing/functions/size.d.ts.map +1 -1
  57. package/dist/parsing/functions/size.js +20 -2
  58. package/dist/parsing/functions/size.js.map +1 -1
  59. package/dist/parsing/functions/split.d.ts.map +1 -1
  60. package/dist/parsing/functions/split.js +21 -2
  61. package/dist/parsing/functions/split.js.map +1 -1
  62. package/dist/parsing/functions/stringify.d.ts.map +1 -1
  63. package/dist/parsing/functions/stringify.js +20 -2
  64. package/dist/parsing/functions/stringify.js.map +1 -1
  65. package/dist/parsing/functions/sum.d.ts.map +1 -1
  66. package/dist/parsing/functions/sum.js +20 -2
  67. package/dist/parsing/functions/sum.js.map +1 -1
  68. package/dist/parsing/functions/to_json.d.ts.map +1 -1
  69. package/dist/parsing/functions/to_json.js +20 -2
  70. package/dist/parsing/functions/to_json.js.map +1 -1
  71. package/dist/parsing/parser.d.ts.map +1 -1
  72. package/dist/parsing/parser.js +1 -2
  73. package/dist/parsing/parser.js.map +1 -1
  74. package/docs/flowquery.min.js +1 -1
  75. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
  76. package/misc/apps/RAG/.env.example +14 -0
  77. package/misc/apps/RAG/README.md +0 -7
  78. package/misc/apps/RAG/package.json +16 -7
  79. package/misc/apps/RAG/public/index.html +18 -0
  80. package/misc/apps/RAG/src/App.css +42 -0
  81. package/misc/apps/RAG/src/App.tsx +50 -0
  82. package/misc/apps/RAG/src/components/ApiKeySettings.tsx +245 -0
  83. package/misc/apps/RAG/src/components/ChatContainer.css +67 -0
  84. package/misc/apps/RAG/src/components/ChatContainer.tsx +239 -0
  85. package/misc/apps/RAG/src/components/ChatInput.css +23 -0
  86. package/misc/apps/RAG/src/components/ChatInput.tsx +62 -0
  87. package/misc/apps/RAG/src/components/ChatMessage.css +136 -0
  88. package/misc/apps/RAG/src/components/ChatMessage.tsx +152 -0
  89. package/misc/apps/RAG/src/components/FlowQueryAgent.ts +390 -0
  90. package/misc/apps/RAG/src/components/FlowQueryRunner.css +104 -0
  91. package/misc/apps/RAG/src/components/FlowQueryRunner.tsx +332 -0
  92. package/misc/apps/RAG/src/components/index.ts +15 -0
  93. package/misc/apps/RAG/src/index.tsx +17 -0
  94. package/misc/apps/RAG/src/plugins/README.md +139 -0
  95. package/misc/apps/RAG/src/plugins/index.ts +68 -0
  96. package/misc/apps/RAG/src/plugins/loaders/CatFacts.ts +75 -0
  97. package/misc/apps/RAG/src/plugins/loaders/FetchJson.ts +67 -0
  98. package/misc/apps/RAG/src/plugins/loaders/Llm.ts +437 -0
  99. package/misc/apps/RAG/src/plugins/loaders/MockData.ts +151 -0
  100. package/misc/apps/RAG/src/prompts/FlowQuerySystemPrompt.ts +385 -0
  101. package/misc/apps/RAG/src/prompts/index.ts +10 -0
  102. package/misc/apps/RAG/src/utils/FlowQueryExecutor.ts +131 -0
  103. package/misc/apps/RAG/src/utils/FlowQueryExtractor.ts +203 -0
  104. package/misc/apps/RAG/src/utils/index.ts +9 -0
  105. package/misc/apps/RAG/tsconfig.json +4 -2
  106. package/misc/apps/RAG/webpack.config.js +23 -12
  107. package/package.json +7 -1
  108. package/src/compute/runner.ts +1 -26
  109. package/src/extensibility.ts +45 -0
  110. package/src/index.browser.ts +2 -88
  111. package/src/index.node.ts +3 -92
  112. package/src/parsing/functions/avg.ts +10 -0
  113. package/src/parsing/functions/collect.ts +10 -0
  114. package/src/parsing/functions/function_factory.ts +56 -194
  115. package/src/parsing/functions/function_metadata.ts +187 -168
  116. package/src/parsing/functions/functions.ts +27 -0
  117. package/src/parsing/functions/join.ts +11 -0
  118. package/src/parsing/functions/predicate_function.ts +4 -0
  119. package/src/parsing/functions/predicate_sum.ts +13 -0
  120. package/src/parsing/functions/rand.ts +8 -0
  121. package/src/parsing/functions/range.ts +11 -0
  122. package/src/parsing/functions/replace.ts +12 -0
  123. package/src/parsing/functions/round.ts +10 -0
  124. package/src/parsing/functions/size.ts +10 -0
  125. package/src/parsing/functions/split.ts +11 -0
  126. package/src/parsing/functions/stringify.ts +10 -0
  127. package/src/parsing/functions/sum.ts +10 -0
  128. package/src/parsing/functions/to_json.ts +10 -0
  129. package/src/parsing/parser.ts +1 -2
  130. package/tests/extensibility.test.ts +563 -0
  131. package/tsconfig.json +1 -0
  132. package/dist/parsing/functions/predicate_function_factory.d.ts +0 -6
  133. package/dist/parsing/functions/predicate_function_factory.d.ts.map +0 -1
  134. package/dist/parsing/functions/predicate_function_factory.js +0 -19
  135. package/dist/parsing/functions/predicate_function_factory.js.map +0 -1
  136. package/misc/apps/RAG/src/index.ts +0 -20
  137. package/src/parsing/functions/predicate_function_factory.ts +0 -15
  138. package/tests/parsing/function_plugins.test.ts +0 -369
@@ -0,0 +1,50 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ FluentProvider,
4
+ webLightTheme,
5
+ Title3,
6
+ } from '@fluentui/react-components';
7
+ import { ApiKeySettings } from './components/ApiKeySettings';
8
+ import { FlowQueryRunner } from './components/FlowQueryRunner';
9
+ import { ChatContainer } from './components/ChatContainer';
10
+ import { generateFlowQuerySystemPrompt } from './prompts';
11
+ import './App.css';
12
+
13
+ const App: React.FC = () => {
14
+ const [systemPrompt, setSystemPrompt] = useState<string>('');
15
+
16
+ useEffect(() => {
17
+ // Generate the system prompt after plugins are initialized
18
+ const prompt = generateFlowQuerySystemPrompt();
19
+ setSystemPrompt(prompt);
20
+ }, []);
21
+
22
+ return (
23
+ <FluentProvider theme={webLightTheme}>
24
+ <div className="app-root">
25
+ <div className="app-container">
26
+ <div className="app-header">
27
+ <Title3 as="h1">
28
+ FlowQuery Assistant
29
+ </Title3>
30
+ <div className="app-header-actions">
31
+ <FlowQueryRunner />
32
+ <ApiKeySettings />
33
+ </div>
34
+ </div>
35
+
36
+ <div className="chat-wrapper">
37
+ <ChatContainer
38
+ systemPrompt={systemPrompt}
39
+ useStreaming={true}
40
+ useFlowQueryAgent={true}
41
+ showIntermediateSteps={true}
42
+ />
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </FluentProvider>
47
+ );
48
+ };
49
+
50
+ export default App;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * API Key Settings Component
3
+ *
4
+ * Allows users to input and store their OpenAI API key in localStorage.
5
+ * The key is never sent to any server - it's only used client-side.
6
+ */
7
+
8
+ import React, { Component } from 'react';
9
+ import {
10
+ Dialog,
11
+ DialogTrigger,
12
+ DialogSurface,
13
+ DialogTitle,
14
+ DialogBody,
15
+ DialogActions,
16
+ DialogContent,
17
+ Button,
18
+ Input,
19
+ Label,
20
+ Field,
21
+ MessageBar,
22
+ MessageBarBody,
23
+ tokens,
24
+ } from '@fluentui/react-components';
25
+ import { Settings24Regular, Key24Regular, Checkmark24Regular } from '@fluentui/react-icons';
26
+
27
+ const STORAGE_KEY = 'flowquery_openai_api_key';
28
+ const ORG_STORAGE_KEY = 'flowquery_openai_org_id';
29
+ const MODEL_STORAGE_KEY = 'flowquery_openai_model';
30
+
31
+ export interface ApiKeyConfig {
32
+ apiKey: string;
33
+ organizationId?: string;
34
+ model?: string;
35
+ }
36
+
37
+ /**
38
+ * Get the stored API configuration from localStorage.
39
+ */
40
+ export function getStoredApiConfig(): ApiKeyConfig | null {
41
+ const apiKey = localStorage.getItem(STORAGE_KEY);
42
+ if (!apiKey) return null;
43
+
44
+ return {
45
+ apiKey,
46
+ organizationId: localStorage.getItem(ORG_STORAGE_KEY) || undefined,
47
+ model: localStorage.getItem(MODEL_STORAGE_KEY) || undefined,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Check if an API key is configured in localStorage.
53
+ */
54
+ export function hasApiKey(): boolean {
55
+ return !!localStorage.getItem(STORAGE_KEY);
56
+ }
57
+
58
+ /**
59
+ * Get just the API key from storage.
60
+ */
61
+ export function getApiKey(): string | null {
62
+ return localStorage.getItem(STORAGE_KEY);
63
+ }
64
+
65
+ interface ApiKeySettingsProps {
66
+ onSave?: (config: ApiKeyConfig) => void;
67
+ }
68
+
69
+ interface ApiKeySettingsState {
70
+ open: boolean;
71
+ apiKey: string;
72
+ organizationId: string;
73
+ model: string;
74
+ saved: boolean;
75
+ }
76
+
77
+ export class ApiKeySettings extends Component<ApiKeySettingsProps, ApiKeySettingsState> {
78
+ constructor(props: ApiKeySettingsProps) {
79
+ super(props);
80
+ this.state = {
81
+ open: false,
82
+ apiKey: '',
83
+ organizationId: '',
84
+ model: '',
85
+ saved: false,
86
+ };
87
+ }
88
+
89
+ componentDidUpdate(_prevProps: ApiKeySettingsProps, prevState: ApiKeySettingsState): void {
90
+ // Load existing values when dialog opens
91
+ if (this.state.open && !prevState.open) {
92
+ const config = getStoredApiConfig();
93
+ this.setState({
94
+ apiKey: config?.apiKey || '',
95
+ organizationId: config?.organizationId || '',
96
+ model: config?.model || '',
97
+ saved: false,
98
+ });
99
+ }
100
+ }
101
+
102
+ handleOpenChange = (_: unknown, data: { open: boolean }): void => {
103
+ this.setState({ open: data.open });
104
+ };
105
+
106
+ handleSave = (): void => {
107
+ const { apiKey, organizationId, model } = this.state;
108
+ const { onSave } = this.props;
109
+
110
+ // Save to localStorage
111
+ if (apiKey) {
112
+ localStorage.setItem(STORAGE_KEY, apiKey);
113
+ } else {
114
+ localStorage.removeItem(STORAGE_KEY);
115
+ }
116
+
117
+ if (organizationId) {
118
+ localStorage.setItem(ORG_STORAGE_KEY, organizationId);
119
+ } else {
120
+ localStorage.removeItem(ORG_STORAGE_KEY);
121
+ }
122
+
123
+ if (model) {
124
+ localStorage.setItem(MODEL_STORAGE_KEY, model);
125
+ } else {
126
+ localStorage.removeItem(MODEL_STORAGE_KEY);
127
+ }
128
+
129
+ const config: ApiKeyConfig = {
130
+ apiKey,
131
+ organizationId: organizationId || undefined,
132
+ model: model || undefined,
133
+ };
134
+
135
+ this.setState({ saved: true });
136
+ onSave?.(config);
137
+
138
+ // Close dialog after a brief delay to show success
139
+ setTimeout(() => this.setState({ open: false }), 500);
140
+ };
141
+
142
+ handleClear = (): void => {
143
+ localStorage.removeItem(STORAGE_KEY);
144
+ localStorage.removeItem(ORG_STORAGE_KEY);
145
+ localStorage.removeItem(MODEL_STORAGE_KEY);
146
+ this.setState({
147
+ apiKey: '',
148
+ organizationId: '',
149
+ model: '',
150
+ saved: false,
151
+ });
152
+ };
153
+
154
+ handleCancel = (): void => {
155
+ this.setState({ open: false });
156
+ };
157
+
158
+ handleApiKeyChange = (_: unknown, data: { value: string }): void => {
159
+ this.setState({ apiKey: data.value });
160
+ };
161
+
162
+ handleOrganizationIdChange = (_: unknown, data: { value: string }): void => {
163
+ this.setState({ organizationId: data.value });
164
+ };
165
+
166
+ handleModelChange = (_: unknown, data: { value: string }): void => {
167
+ this.setState({ model: data.value });
168
+ };
169
+
170
+ render(): React.ReactNode {
171
+ const { open, apiKey, organizationId, model, saved } = this.state;
172
+ const isConfigured = hasApiKey();
173
+
174
+ return (
175
+ <Dialog open={open} onOpenChange={this.handleOpenChange}>
176
+ <DialogTrigger disableButtonEnhancement>
177
+ <Button
178
+ appearance={isConfigured ? 'subtle' : 'primary'}
179
+ icon={isConfigured ? <Checkmark24Regular /> : <Settings24Regular />}
180
+ title="API Settings"
181
+ >
182
+ {isConfigured ? 'API Configured' : 'Configure API Key'}
183
+ </Button>
184
+ </DialogTrigger>
185
+ <DialogSurface>
186
+ <DialogBody>
187
+ <DialogTitle>OpenAI API Settings</DialogTitle>
188
+ <DialogContent style={{ display: 'flex', flexDirection: 'column', gap: '16px', paddingTop: '16px' }}>
189
+ <MessageBar intent="info">
190
+ <MessageBarBody>
191
+ Your API key is stored locally in your browser and never sent to any server.
192
+ </MessageBarBody>
193
+ </MessageBar>
194
+
195
+ <Field label="API Key" required>
196
+ <Input
197
+ type="password"
198
+ value={apiKey}
199
+ onChange={this.handleApiKeyChange}
200
+ placeholder="sk-..."
201
+ contentBefore={<Key24Regular />}
202
+ />
203
+ </Field>
204
+
205
+ <Field label="Organization ID" hint="Optional - only needed for organization accounts">
206
+ <Input
207
+ value={organizationId}
208
+ onChange={this.handleOrganizationIdChange}
209
+ placeholder="org-..."
210
+ />
211
+ </Field>
212
+
213
+ <Field label="Default Model" hint="Optional - defaults to gpt-4o-mini">
214
+ <Input
215
+ value={model}
216
+ onChange={this.handleModelChange}
217
+ placeholder="gpt-4o-mini"
218
+ />
219
+ </Field>
220
+
221
+ {saved && (
222
+ <MessageBar intent="success">
223
+ <MessageBarBody>Settings saved successfully!</MessageBarBody>
224
+ </MessageBar>
225
+ )}
226
+ </DialogContent>
227
+ <DialogActions>
228
+ <Button appearance="secondary" onClick={this.handleClear}>
229
+ Clear
230
+ </Button>
231
+ <Button appearance="secondary" onClick={this.handleCancel}>
232
+ Cancel
233
+ </Button>
234
+ <Button appearance="primary" onClick={this.handleSave} disabled={!apiKey}>
235
+ Save
236
+ </Button>
237
+ </DialogActions>
238
+ </DialogBody>
239
+ </DialogSurface>
240
+ </Dialog>
241
+ );
242
+ }
243
+ }
244
+
245
+ export default ApiKeySettings;
@@ -0,0 +1,67 @@
1
+ .chat-container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ background-color: #ffffff;
6
+ border-radius: 8px;
7
+ overflow: hidden;
8
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
9
+ }
10
+
11
+ .chat-messages {
12
+ flex: 1;
13
+ overflow-y: auto;
14
+ padding: 16px;
15
+ background-color: #fafafa;
16
+ }
17
+
18
+ .chat-empty-state {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ height: 100%;
23
+ color: #666;
24
+ text-align: center;
25
+ padding: 24px;
26
+ }
27
+
28
+ .chat-empty-state p {
29
+ margin: 0;
30
+ font-size: 16px;
31
+ }
32
+
33
+ .chat-loading {
34
+ display: flex;
35
+ justify-content: center;
36
+ padding: 16px;
37
+ }
38
+
39
+ .chat-error {
40
+ padding: 12px 16px;
41
+ background-color: #fde7e9;
42
+ color: #c50f1f;
43
+ border-top: 1px solid #c50f1f;
44
+ font-size: 14px;
45
+ }
46
+
47
+ .chat-clear-button {
48
+ padding: 8px 16px;
49
+ margin: 8px 16px 16px;
50
+ background-color: transparent;
51
+ border: 1px solid #d1d1d1;
52
+ border-radius: 4px;
53
+ color: #666;
54
+ cursor: pointer;
55
+ font-size: 14px;
56
+ transition: all 0.2s ease;
57
+ }
58
+
59
+ .chat-clear-button:hover:not(:disabled) {
60
+ background-color: #f5f5f5;
61
+ border-color: #b3b3b3;
62
+ }
63
+
64
+ .chat-clear-button:disabled {
65
+ opacity: 0.5;
66
+ cursor: not-allowed;
67
+ }
@@ -0,0 +1,239 @@
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { Spinner } from '@fluentui/react-components';
3
+ import { ChatMessage, Message } from './ChatMessage';
4
+ import { ChatInput } from './ChatInput';
5
+ import { LlmOptions } from '../plugins/loaders/Llm';
6
+ import { processQueryStream } from './FlowQueryAgent';
7
+ import './ChatContainer.css';
8
+
9
+ interface ChatContainerProps {
10
+ systemPrompt?: string;
11
+ llmOptions?: LlmOptions;
12
+ useStreaming?: boolean;
13
+ /** Whether to use the FlowQuery agent for processing queries */
14
+ useFlowQueryAgent?: boolean;
15
+ /** Whether to show intermediate steps (query generation, execution) */
16
+ showIntermediateSteps?: boolean;
17
+ }
18
+
19
+ export const ChatContainer: React.FC<ChatContainerProps> = ({
20
+ systemPrompt = 'You are a helpful assistant. Be concise and informative in your responses.',
21
+ llmOptions = {},
22
+ useStreaming = true,
23
+ useFlowQueryAgent = true,
24
+ showIntermediateSteps = true
25
+ }) => {
26
+ const [messages, setMessages] = useState<Message[]>([]);
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+ const messagesEndRef = useRef<HTMLDivElement>(null);
30
+
31
+ const scrollToBottom = useCallback(() => {
32
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ scrollToBottom();
37
+ }, [messages, scrollToBottom]);
38
+
39
+ const generateMessageId = () => `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
40
+
41
+ const buildConversationHistory = useCallback((currentMessages: Message[]) => {
42
+ return currentMessages.map(msg => ({
43
+ role: msg.role as 'user' | 'assistant',
44
+ content: msg.content
45
+ }));
46
+ }, []);
47
+
48
+ const handleSendMessage = useCallback(async (content: string) => {
49
+ const userMessage: Message = {
50
+ id: generateMessageId(),
51
+ role: 'user',
52
+ content,
53
+ timestamp: new Date()
54
+ };
55
+
56
+ setMessages(prev => [...prev, userMessage]);
57
+ setIsLoading(true);
58
+ setError(null);
59
+
60
+ const assistantMessageId = generateMessageId();
61
+
62
+ try {
63
+ const conversationHistory = buildConversationHistory([...messages, userMessage]);
64
+
65
+ if (useFlowQueryAgent) {
66
+ // Use the FlowQuery agent for processing
67
+ const assistantMessage: Message = {
68
+ id: assistantMessageId,
69
+ role: 'assistant',
70
+ content: '',
71
+ timestamp: new Date(),
72
+ isStreaming: true
73
+ };
74
+ setMessages(prev => [...prev, assistantMessage]);
75
+
76
+ let fullContent = '';
77
+
78
+ for await (const { chunk, done } of processQueryStream(content, {
79
+ systemPrompt,
80
+ llmOptions,
81
+ conversationHistory: conversationHistory.slice(0, -1),
82
+ showIntermediateSteps
83
+ })) {
84
+ if (chunk) {
85
+ fullContent += chunk;
86
+ setMessages(prev =>
87
+ prev.map(msg =>
88
+ msg.id === assistantMessageId
89
+ ? { ...msg, content: fullContent }
90
+ : msg
91
+ )
92
+ );
93
+ }
94
+
95
+ if (done) {
96
+ setMessages(prev =>
97
+ prev.map(msg =>
98
+ msg.id === assistantMessageId
99
+ ? { ...msg, isStreaming: false }
100
+ : msg
101
+ )
102
+ );
103
+ }
104
+ }
105
+ } else {
106
+ // Original LLM-only behavior (kept for backward compatibility)
107
+ const { llm, llmStream } = await import('../plugins/loaders/Llm');
108
+
109
+ const options: LlmOptions = {
110
+ ...llmOptions,
111
+ systemPrompt,
112
+ messages: conversationHistory.slice(0, -1),
113
+ };
114
+
115
+ if (useStreaming) {
116
+ const assistantMessage: Message = {
117
+ id: assistantMessageId,
118
+ role: 'assistant',
119
+ content: '',
120
+ timestamp: new Date(),
121
+ isStreaming: true
122
+ };
123
+ setMessages(prev => [...prev, assistantMessage]);
124
+
125
+ let fullContent = '';
126
+ for await (const chunk of llmStream(content, options)) {
127
+ const deltaContent = chunk.choices?.[0]?.delta?.content || '';
128
+ if (deltaContent) {
129
+ fullContent += deltaContent;
130
+ setMessages(prev =>
131
+ prev.map(msg =>
132
+ msg.id === assistantMessageId
133
+ ? { ...msg, content: fullContent }
134
+ : msg
135
+ )
136
+ );
137
+ }
138
+ }
139
+
140
+ setMessages(prev =>
141
+ prev.map(msg =>
142
+ msg.id === assistantMessageId
143
+ ? { ...msg, isStreaming: false }
144
+ : msg
145
+ )
146
+ );
147
+ } else {
148
+ const response = await llm(content, options);
149
+ const assistantContent = response.choices[0]?.message?.content || 'No response received.';
150
+
151
+ const assistantMessage: Message = {
152
+ id: assistantMessageId,
153
+ role: 'assistant',
154
+ content: assistantContent,
155
+ timestamp: new Date()
156
+ };
157
+ setMessages(prev => [...prev, assistantMessage]);
158
+ }
159
+ }
160
+ } catch (err) {
161
+ const errorMessage = err instanceof Error ? err.message : 'An error occurred while processing your request.';
162
+ setError(errorMessage);
163
+
164
+ // Add or update error message as assistant response
165
+ const errorContent = `⚠️ Error: ${errorMessage}`;
166
+ setMessages(prev => {
167
+ // Check if we already added a streaming message with this ID
168
+ const existingMessageIndex = prev.findIndex(msg => msg.id === assistantMessageId);
169
+ if (existingMessageIndex !== -1) {
170
+ // Update existing message
171
+ return prev.map(msg =>
172
+ msg.id === assistantMessageId
173
+ ? { ...msg, content: errorContent, isStreaming: false }
174
+ : msg
175
+ );
176
+ } else {
177
+ // Add new error message
178
+ return [...prev, {
179
+ id: assistantMessageId,
180
+ role: 'assistant' as const,
181
+ content: errorContent,
182
+ timestamp: new Date()
183
+ }];
184
+ }
185
+ });
186
+ } finally {
187
+ setIsLoading(false);
188
+ }
189
+ }, [messages, systemPrompt, llmOptions, useStreaming, useFlowQueryAgent, showIntermediateSteps, buildConversationHistory]);
190
+
191
+ const handleClearChat = useCallback(() => {
192
+ setMessages([]);
193
+ setError(null);
194
+ }, []);
195
+
196
+ return (
197
+ <div className="chat-container">
198
+ <div className="chat-messages">
199
+ {messages.length === 0 ? (
200
+ <div className="chat-empty-state">
201
+ <p>Start a conversation by typing a message below.</p>
202
+ </div>
203
+ ) : (
204
+ messages.map(message => (
205
+ <ChatMessage key={message.id} message={message} />
206
+ ))
207
+ )}
208
+ {isLoading && messages[messages.length - 1]?.role === 'user' && (
209
+ <div className="chat-loading">
210
+ <Spinner size="small" label="Thinking..." />
211
+ </div>
212
+ )}
213
+ <div ref={messagesEndRef} />
214
+ </div>
215
+
216
+ {error && !messages.some(m => m.content.includes(error)) && (
217
+ <div className="chat-error">
218
+ {error}
219
+ </div>
220
+ )}
221
+
222
+ <ChatInput
223
+ onSendMessage={handleSendMessage}
224
+ isLoading={isLoading}
225
+ placeholder="Ask me anything..."
226
+ />
227
+
228
+ {messages.length > 0 && (
229
+ <button
230
+ className="chat-clear-button"
231
+ onClick={handleClearChat}
232
+ disabled={isLoading}
233
+ >
234
+ Clear conversation
235
+ </button>
236
+ )}
237
+ </div>
238
+ );
239
+ };
@@ -0,0 +1,23 @@
1
+ .chat-input-container {
2
+ display: flex;
3
+ gap: 8px;
4
+ padding: 16px;
5
+ background-color: #ffffff;
6
+ border-top: 1px solid #e0e0e0;
7
+ align-items: flex-end;
8
+ }
9
+
10
+ .chat-input-textarea {
11
+ flex: 1;
12
+ min-height: 44px;
13
+ max-height: 150px;
14
+ }
15
+
16
+ .chat-input-textarea textarea {
17
+ min-height: 44px;
18
+ }
19
+
20
+ .chat-input-send-button {
21
+ height: 44px;
22
+ min-width: 44px;
23
+ }
@@ -0,0 +1,62 @@
1
+ import React, { useState, useCallback, KeyboardEvent } from 'react';
2
+ import { Textarea, Button } from '@fluentui/react-components';
3
+ import { SendFilled } from '@fluentui/react-icons';
4
+ import './ChatInput.css';
5
+
6
+ interface ChatInputProps {
7
+ onSendMessage: (message: string) => void;
8
+ isLoading: boolean;
9
+ placeholder?: string;
10
+ }
11
+
12
+ export const ChatInput: React.FC<ChatInputProps> = ({
13
+ onSendMessage,
14
+ isLoading,
15
+ placeholder = 'Ask me anything...'
16
+ }) => {
17
+ const [inputValue, setInputValue] = useState('');
18
+
19
+ const handleSend = useCallback(() => {
20
+ const trimmedValue = inputValue.trim();
21
+ if (trimmedValue && !isLoading) {
22
+ onSendMessage(trimmedValue);
23
+ setInputValue('');
24
+ }
25
+ }, [inputValue, isLoading, onSendMessage]);
26
+
27
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
28
+ if (e.key === 'Enter' && !e.shiftKey) {
29
+ e.preventDefault();
30
+ handleSend();
31
+ }
32
+ }, [handleSend]);
33
+
34
+ const handleChange = useCallback((
35
+ _e: React.ChangeEvent<HTMLTextAreaElement>,
36
+ data: { value: string }
37
+ ) => {
38
+ setInputValue(data.value);
39
+ }, []);
40
+
41
+ return (
42
+ <div className="chat-input-container">
43
+ <Textarea
44
+ className="chat-input-textarea"
45
+ value={inputValue}
46
+ onChange={handleChange}
47
+ onKeyDown={handleKeyDown}
48
+ placeholder={placeholder}
49
+ resize="none"
50
+ disabled={isLoading}
51
+ />
52
+ <Button
53
+ className="chat-input-send-button"
54
+ appearance="primary"
55
+ icon={<SendFilled />}
56
+ onClick={handleSend}
57
+ disabled={isLoading || !inputValue.trim()}
58
+ title="Send message (Enter)"
59
+ />
60
+ </div>
61
+ );
62
+ };