flowquery 1.0.5 → 1.0.6
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/README.md +182 -0
- package/dist/extensibility.d.ts +9 -0
- package/dist/extensibility.d.ts.map +1 -0
- package/dist/extensibility.js +25 -0
- package/dist/extensibility.js.map +1 -0
- package/dist/flowquery.min.js +1 -1
- package/dist/parsing/functions/avg.d.ts.map +1 -1
- package/dist/parsing/functions/avg.js +20 -2
- package/dist/parsing/functions/avg.js.map +1 -1
- package/dist/parsing/functions/collect.d.ts.map +1 -1
- package/dist/parsing/functions/collect.js +20 -2
- package/dist/parsing/functions/collect.js.map +1 -1
- package/dist/parsing/functions/extensibility/index.d.ts +37 -0
- package/dist/parsing/functions/extensibility/index.d.ts.map +1 -0
- package/dist/parsing/functions/extensibility/index.js +50 -0
- package/dist/parsing/functions/extensibility/index.js.map +1 -0
- package/dist/parsing/functions/function_factory.d.ts +23 -0
- package/dist/parsing/functions/function_factory.d.ts.map +1 -1
- package/dist/parsing/functions/function_factory.js +44 -47
- package/dist/parsing/functions/function_factory.js.map +1 -1
- package/dist/parsing/functions/function_metadata.d.ts +57 -6
- package/dist/parsing/functions/function_metadata.d.ts.map +1 -1
- package/dist/parsing/functions/function_metadata.js +103 -153
- package/dist/parsing/functions/function_metadata.js.map +1 -1
- package/dist/parsing/functions/functions.d.ts.map +1 -1
- package/dist/parsing/functions/functions.js +37 -2
- package/dist/parsing/functions/functions.js.map +1 -1
- package/dist/parsing/functions/join.d.ts.map +1 -1
- package/dist/parsing/functions/join.js +21 -2
- package/dist/parsing/functions/join.js.map +1 -1
- package/dist/parsing/functions/predicate_function.d.ts +1 -0
- package/dist/parsing/functions/predicate_function.d.ts.map +1 -1
- package/dist/parsing/functions/predicate_function.js +3 -0
- package/dist/parsing/functions/predicate_function.js.map +1 -1
- package/dist/parsing/functions/predicate_sum.d.ts.map +1 -1
- package/dist/parsing/functions/predicate_sum.js +23 -2
- package/dist/parsing/functions/predicate_sum.js.map +1 -1
- package/dist/parsing/functions/rand.d.ts.map +1 -1
- package/dist/parsing/functions/rand.js +18 -2
- package/dist/parsing/functions/rand.js.map +1 -1
- package/dist/parsing/functions/range.d.ts.map +1 -1
- package/dist/parsing/functions/range.js +21 -2
- package/dist/parsing/functions/range.js.map +1 -1
- package/dist/parsing/functions/replace.d.ts.map +1 -1
- package/dist/parsing/functions/replace.js +22 -2
- package/dist/parsing/functions/replace.js.map +1 -1
- package/dist/parsing/functions/round.d.ts.map +1 -1
- package/dist/parsing/functions/round.js +20 -2
- package/dist/parsing/functions/round.js.map +1 -1
- package/dist/parsing/functions/size.d.ts.map +1 -1
- package/dist/parsing/functions/size.js +20 -2
- package/dist/parsing/functions/size.js.map +1 -1
- package/dist/parsing/functions/split.d.ts.map +1 -1
- package/dist/parsing/functions/split.js +21 -2
- package/dist/parsing/functions/split.js.map +1 -1
- package/dist/parsing/functions/stringify.d.ts.map +1 -1
- package/dist/parsing/functions/stringify.js +20 -2
- package/dist/parsing/functions/stringify.js.map +1 -1
- package/dist/parsing/functions/sum.d.ts.map +1 -1
- package/dist/parsing/functions/sum.js +20 -2
- package/dist/parsing/functions/sum.js.map +1 -1
- package/dist/parsing/functions/to_json.d.ts.map +1 -1
- package/dist/parsing/functions/to_json.js +20 -2
- package/dist/parsing/functions/to_json.js.map +1 -1
- package/dist/parsing/parser.d.ts.map +1 -1
- package/dist/parsing/parser.js +1 -2
- package/dist/parsing/parser.js.map +1 -1
- package/docs/flowquery.min.js +1 -1
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -1
- package/misc/apps/RAG/.env.example +14 -0
- package/misc/apps/RAG/README.md +0 -7
- package/misc/apps/RAG/package.json +16 -7
- package/misc/apps/RAG/public/index.html +18 -0
- package/misc/apps/RAG/src/App.css +42 -0
- package/misc/apps/RAG/src/App.tsx +50 -0
- package/misc/apps/RAG/src/components/ApiKeySettings.tsx +245 -0
- package/misc/apps/RAG/src/components/ChatContainer.css +67 -0
- package/misc/apps/RAG/src/components/ChatContainer.tsx +239 -0
- package/misc/apps/RAG/src/components/ChatInput.css +23 -0
- package/misc/apps/RAG/src/components/ChatInput.tsx +62 -0
- package/misc/apps/RAG/src/components/ChatMessage.css +136 -0
- package/misc/apps/RAG/src/components/ChatMessage.tsx +152 -0
- package/misc/apps/RAG/src/components/FlowQueryAgent.ts +390 -0
- package/misc/apps/RAG/src/components/FlowQueryRunner.css +104 -0
- package/misc/apps/RAG/src/components/FlowQueryRunner.tsx +332 -0
- package/misc/apps/RAG/src/components/index.ts +15 -0
- package/misc/apps/RAG/src/index.tsx +17 -0
- package/misc/apps/RAG/src/plugins/PluginRegistry.ts +136 -0
- package/misc/apps/RAG/src/plugins/README.md +139 -0
- package/misc/apps/RAG/src/plugins/index.ts +72 -0
- package/misc/apps/RAG/src/plugins/loaders/CatFacts.ts +79 -0
- package/misc/apps/RAG/src/plugins/loaders/FetchJson.ts +71 -0
- package/misc/apps/RAG/src/plugins/loaders/Llm.ts +441 -0
- package/misc/apps/RAG/src/plugins/loaders/MockData.ts +161 -0
- package/misc/apps/RAG/src/plugins/types.ts +52 -0
- package/misc/apps/RAG/src/prompts/FlowQuerySystemPrompt.ts +385 -0
- package/misc/apps/RAG/src/prompts/index.ts +10 -0
- package/misc/apps/RAG/src/utils/FlowQueryExecutor.ts +131 -0
- package/misc/apps/RAG/src/utils/FlowQueryExtractor.ts +203 -0
- package/misc/apps/RAG/src/utils/index.ts +9 -0
- package/misc/apps/RAG/tsconfig.json +4 -2
- package/misc/apps/RAG/webpack.config.js +23 -12
- package/package.json +7 -1
- package/src/extensibility.ts +9 -0
- package/src/parsing/functions/avg.ts +10 -0
- package/src/parsing/functions/collect.ts +10 -0
- package/src/parsing/functions/extensibility/index.ts +54 -0
- package/src/parsing/functions/function_factory.ts +51 -48
- package/src/parsing/functions/function_metadata.ts +132 -156
- package/src/parsing/functions/functions.ts +27 -0
- package/src/parsing/functions/join.ts +11 -0
- package/src/parsing/functions/predicate_function.ts +4 -0
- package/src/parsing/functions/predicate_sum.ts +13 -0
- package/src/parsing/functions/rand.ts +8 -0
- package/src/parsing/functions/range.ts +11 -0
- package/src/parsing/functions/replace.ts +12 -0
- package/src/parsing/functions/round.ts +10 -0
- package/src/parsing/functions/size.ts +10 -0
- package/src/parsing/functions/split.ts +11 -0
- package/src/parsing/functions/stringify.ts +10 -0
- package/src/parsing/functions/sum.ts +10 -0
- package/src/parsing/functions/to_json.ts +10 -0
- package/src/parsing/parser.ts +1 -2
- package/tests/parsing/function_plugins.test.ts +11 -11
- package/tsconfig.json +1 -0
- package/dist/parsing/functions/predicate_function_factory.d.ts +0 -6
- package/dist/parsing/functions/predicate_function_factory.d.ts.map +0 -1
- package/dist/parsing/functions/predicate_function_factory.js +0 -19
- package/dist/parsing/functions/predicate_function_factory.js.map +0 -1
- package/misc/apps/RAG/src/index.ts +0 -20
- package/src/parsing/functions/predicate_function_factory.ts +0 -15
|
@@ -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
|
+
};
|