@gendive/chatllm 0.6.12 → 0.7.0
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/react/index.d.mts +99 -2
- package/dist/react/index.d.ts +99 -2
- package/dist/react/index.js +754 -88
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +746 -80
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -48,10 +48,10 @@ __export(index_exports, {
|
|
|
48
48
|
module.exports = __toCommonJS(index_exports);
|
|
49
49
|
|
|
50
50
|
// src/react/ChatUI.tsx
|
|
51
|
-
var
|
|
51
|
+
var import_react12 = __toESM(require("react"));
|
|
52
52
|
|
|
53
53
|
// src/react/hooks/useChatUI.ts
|
|
54
|
-
var
|
|
54
|
+
var import_react3 = require("react");
|
|
55
55
|
|
|
56
56
|
// src/types.ts
|
|
57
57
|
var DEFAULT_PERSONALIZATION = {
|
|
@@ -67,10 +67,481 @@ var DEFAULT_PERSONALIZATION = {
|
|
|
67
67
|
language: "auto"
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
// src/react/hooks/useGlobalMemory.ts
|
|
71
|
+
var import_react = require("react");
|
|
72
|
+
|
|
73
|
+
// src/react/adapters/LocalStorageAdapter.ts
|
|
74
|
+
var LocalStorageAdapter = class {
|
|
75
|
+
storageKey;
|
|
76
|
+
constructor(storageKey = "chatllm_global_memory") {
|
|
77
|
+
this.storageKey = storageKey;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @description 전체 데이터 로드
|
|
81
|
+
*/
|
|
82
|
+
loadData() {
|
|
83
|
+
if (typeof window === "undefined") return {};
|
|
84
|
+
try {
|
|
85
|
+
const data = localStorage.getItem(this.storageKey);
|
|
86
|
+
return data ? JSON.parse(data) : {};
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* @description 전체 데이터 저장
|
|
93
|
+
*/
|
|
94
|
+
saveData(data) {
|
|
95
|
+
if (typeof window === "undefined") return;
|
|
96
|
+
try {
|
|
97
|
+
localStorage.setItem(this.storageKey, JSON.stringify(data));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("[LocalStorageAdapter] Failed to save data:", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async read(key) {
|
|
103
|
+
const data = this.loadData();
|
|
104
|
+
const value = data[key];
|
|
105
|
+
return value !== void 0 ? value : null;
|
|
106
|
+
}
|
|
107
|
+
async write(key, value) {
|
|
108
|
+
const data = this.loadData();
|
|
109
|
+
data[key] = value;
|
|
110
|
+
this.saveData(data);
|
|
111
|
+
}
|
|
112
|
+
async delete(key) {
|
|
113
|
+
const data = this.loadData();
|
|
114
|
+
if (key in data) {
|
|
115
|
+
delete data[key];
|
|
116
|
+
this.saveData(data);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
async exists(key) {
|
|
122
|
+
const data = this.loadData();
|
|
123
|
+
return key in data;
|
|
124
|
+
}
|
|
125
|
+
async getAll() {
|
|
126
|
+
const data = this.loadData();
|
|
127
|
+
return new Map(Object.entries(data));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @description 전체 삭제
|
|
131
|
+
*/
|
|
132
|
+
async clear() {
|
|
133
|
+
if (typeof window === "undefined") return;
|
|
134
|
+
localStorage.removeItem(this.storageKey);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/react/adapters/PostgreSQLAdapter.ts
|
|
139
|
+
var PostgreSQLAdapter = class {
|
|
140
|
+
apiEndpoint;
|
|
141
|
+
userId;
|
|
142
|
+
authToken;
|
|
143
|
+
constructor(apiEndpoint, userId, authToken) {
|
|
144
|
+
this.apiEndpoint = apiEndpoint.replace(/\/$/, "");
|
|
145
|
+
this.userId = userId;
|
|
146
|
+
this.authToken = authToken;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* @description HTTP 헤더 생성
|
|
150
|
+
*/
|
|
151
|
+
getHeaders() {
|
|
152
|
+
const headers = {
|
|
153
|
+
"Content-Type": "application/json"
|
|
154
|
+
};
|
|
155
|
+
if (this.authToken) {
|
|
156
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
157
|
+
}
|
|
158
|
+
return headers;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* @description API 요청 헬퍼
|
|
162
|
+
*/
|
|
163
|
+
async request(path, options = {}) {
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(`${this.apiEndpoint}${path}`, {
|
|
166
|
+
...options,
|
|
167
|
+
headers: {
|
|
168
|
+
...this.getHeaders(),
|
|
169
|
+
...options.headers
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
if (response.status === 404) return null;
|
|
174
|
+
throw new Error(`API error: ${response.status}`);
|
|
175
|
+
}
|
|
176
|
+
const contentType = response.headers.get("content-type");
|
|
177
|
+
if (contentType?.includes("application/json")) {
|
|
178
|
+
return response.json();
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("[PostgreSQLAdapter] Request failed:", error);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async read(key) {
|
|
187
|
+
const result = await this.request(
|
|
188
|
+
`/memory/${this.userId}/${encodeURIComponent(key)}`
|
|
189
|
+
);
|
|
190
|
+
return result?.value ?? null;
|
|
191
|
+
}
|
|
192
|
+
async write(key, value) {
|
|
193
|
+
await this.request(`/memory/${this.userId}`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: JSON.stringify({ key, value })
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async delete(key) {
|
|
199
|
+
const result = await this.request(
|
|
200
|
+
`/memory/${this.userId}/${encodeURIComponent(key)}`,
|
|
201
|
+
{ method: "DELETE" }
|
|
202
|
+
);
|
|
203
|
+
return result?.success ?? false;
|
|
204
|
+
}
|
|
205
|
+
async exists(key) {
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch(
|
|
208
|
+
`${this.apiEndpoint}/memory/${this.userId}/${encodeURIComponent(key)}`,
|
|
209
|
+
{
|
|
210
|
+
method: "HEAD",
|
|
211
|
+
headers: this.getHeaders()
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
return response.ok;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async getAll() {
|
|
220
|
+
const result = await this.request(
|
|
221
|
+
`/memory/${this.userId}`
|
|
222
|
+
);
|
|
223
|
+
if (!result?.entries) {
|
|
224
|
+
return /* @__PURE__ */ new Map();
|
|
225
|
+
}
|
|
226
|
+
return new Map(Object.entries(result.entries));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* @description 전체 삭제
|
|
230
|
+
*/
|
|
231
|
+
async clear() {
|
|
232
|
+
await this.request(`/memory/${this.userId}`, {
|
|
233
|
+
method: "DELETE"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/react/hooks/useGlobalMemory.ts
|
|
239
|
+
function createAdapter(options) {
|
|
240
|
+
switch (options.storageType) {
|
|
241
|
+
case "postgresql":
|
|
242
|
+
if (!options.apiEndpoint || !options.userId) {
|
|
243
|
+
console.warn("[useGlobalMemory] PostgreSQL requires apiEndpoint and userId, falling back to localStorage");
|
|
244
|
+
return new LocalStorageAdapter(options.storageKey);
|
|
245
|
+
}
|
|
246
|
+
return new PostgreSQLAdapter(options.apiEndpoint, options.userId, options.authToken);
|
|
247
|
+
case "localStorage":
|
|
248
|
+
case "memory":
|
|
249
|
+
default:
|
|
250
|
+
return new LocalStorageAdapter(options.storageKey);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function useGlobalMemory(options) {
|
|
254
|
+
const [state, setState] = (0, import_react.useState)({
|
|
255
|
+
entries: /* @__PURE__ */ new Map(),
|
|
256
|
+
isLoading: true,
|
|
257
|
+
error: null
|
|
258
|
+
});
|
|
259
|
+
const adapterRef = (0, import_react.useRef)(createAdapter(options));
|
|
260
|
+
const refresh = (0, import_react.useCallback)(async () => {
|
|
261
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
262
|
+
try {
|
|
263
|
+
const allData = await adapterRef.current.getAll();
|
|
264
|
+
const entries = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const [key, value] of allData) {
|
|
266
|
+
if (typeof value === "object" && value !== null && "value" in value) {
|
|
267
|
+
entries.set(key, value);
|
|
268
|
+
} else {
|
|
269
|
+
entries.set(key, {
|
|
270
|
+
key,
|
|
271
|
+
value,
|
|
272
|
+
createdAt: Date.now(),
|
|
273
|
+
updatedAt: Date.now()
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
setState({ entries, isLoading: false, error: null });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
setState((prev) => ({
|
|
280
|
+
...prev,
|
|
281
|
+
isLoading: false,
|
|
282
|
+
error: error instanceof Error ? error : new Error("Unknown error")
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
}, []);
|
|
286
|
+
const set = (0, import_react.useCallback)(
|
|
287
|
+
async (key, value, meta) => {
|
|
288
|
+
const now = Date.now();
|
|
289
|
+
const existingEntry = state.entries.get(key);
|
|
290
|
+
const entry = {
|
|
291
|
+
key,
|
|
292
|
+
value,
|
|
293
|
+
createdAt: existingEntry?.createdAt ?? now,
|
|
294
|
+
updatedAt: now,
|
|
295
|
+
category: meta?.category ?? existingEntry?.category,
|
|
296
|
+
confidence: meta?.confidence ?? existingEntry?.confidence,
|
|
297
|
+
source: meta?.source ?? existingEntry?.source
|
|
298
|
+
};
|
|
299
|
+
try {
|
|
300
|
+
await adapterRef.current.write(key, entry);
|
|
301
|
+
setState((prev) => {
|
|
302
|
+
const newEntries = new Map(prev.entries);
|
|
303
|
+
newEntries.set(key, entry);
|
|
304
|
+
return { ...prev, entries: newEntries };
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error("[useGlobalMemory] Failed to set:", error);
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
[state.entries]
|
|
311
|
+
);
|
|
312
|
+
const get = (0, import_react.useCallback)(
|
|
313
|
+
(key) => {
|
|
314
|
+
const entry = state.entries.get(key);
|
|
315
|
+
return entry?.value;
|
|
316
|
+
},
|
|
317
|
+
[state.entries]
|
|
318
|
+
);
|
|
319
|
+
const remove = (0, import_react.useCallback)(async (key) => {
|
|
320
|
+
try {
|
|
321
|
+
await adapterRef.current.delete(key);
|
|
322
|
+
setState((prev) => {
|
|
323
|
+
const newEntries = new Map(prev.entries);
|
|
324
|
+
newEntries.delete(key);
|
|
325
|
+
return { ...prev, entries: newEntries };
|
|
326
|
+
});
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error("[useGlobalMemory] Failed to remove:", error);
|
|
329
|
+
}
|
|
330
|
+
}, []);
|
|
331
|
+
const has = (0, import_react.useCallback)(
|
|
332
|
+
(key) => {
|
|
333
|
+
return state.entries.has(key);
|
|
334
|
+
},
|
|
335
|
+
[state.entries]
|
|
336
|
+
);
|
|
337
|
+
const clear = (0, import_react.useCallback)(async () => {
|
|
338
|
+
try {
|
|
339
|
+
for (const key of state.entries.keys()) {
|
|
340
|
+
await adapterRef.current.delete(key);
|
|
341
|
+
}
|
|
342
|
+
setState((prev) => ({ ...prev, entries: /* @__PURE__ */ new Map() }));
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error("[useGlobalMemory] Failed to clear:", error);
|
|
345
|
+
}
|
|
346
|
+
}, [state.entries]);
|
|
347
|
+
const toPromptContext = (0, import_react.useCallback)(() => {
|
|
348
|
+
if (state.entries.size === 0) return "";
|
|
349
|
+
const lines = ["[\uC0AC\uC6A9\uC790 \uAE30\uC5B5 \uC815\uBCF4]"];
|
|
350
|
+
const categorized = {
|
|
351
|
+
basic: [],
|
|
352
|
+
professional: [],
|
|
353
|
+
preferences: [],
|
|
354
|
+
context: [],
|
|
355
|
+
other: []
|
|
356
|
+
};
|
|
357
|
+
for (const [key, entry] of state.entries) {
|
|
358
|
+
const category = entry.category || "other";
|
|
359
|
+
if (category in categorized) {
|
|
360
|
+
categorized[category].push({ key, value: entry.value });
|
|
361
|
+
} else {
|
|
362
|
+
categorized.other.push({ key, value: entry.value });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const categoryLabels2 = {
|
|
366
|
+
basic: "\uAE30\uBCF8 \uC815\uBCF4",
|
|
367
|
+
professional: "\uC9C1\uC5C5/\uC804\uBB38 \uC815\uBCF4",
|
|
368
|
+
preferences: "\uC120\uD638\uB3C4",
|
|
369
|
+
context: "\uD604\uC7AC \uB9E5\uB77D",
|
|
370
|
+
other: "\uAE30\uD0C0"
|
|
371
|
+
};
|
|
372
|
+
for (const [category, items] of Object.entries(categorized)) {
|
|
373
|
+
if (items.length > 0) {
|
|
374
|
+
lines.push(`
|
|
375
|
+
${categoryLabels2[category]}:`);
|
|
376
|
+
for (const { key, value } of items) {
|
|
377
|
+
const valueStr = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
378
|
+
lines.push(`- ${key}: ${valueStr}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return lines.join("\n");
|
|
383
|
+
}, [state.entries]);
|
|
384
|
+
(0, import_react.useEffect)(() => {
|
|
385
|
+
refresh();
|
|
386
|
+
}, [refresh]);
|
|
387
|
+
(0, import_react.useEffect)(() => {
|
|
388
|
+
adapterRef.current = createAdapter(options);
|
|
389
|
+
refresh();
|
|
390
|
+
}, [options.storageType, options.storageKey, options.apiEndpoint, options.userId]);
|
|
391
|
+
return {
|
|
392
|
+
state,
|
|
393
|
+
set,
|
|
394
|
+
get,
|
|
395
|
+
remove,
|
|
396
|
+
has,
|
|
397
|
+
clear,
|
|
398
|
+
toPromptContext,
|
|
399
|
+
refresh
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/react/hooks/useInfoExtraction.ts
|
|
404
|
+
var import_react2 = require("react");
|
|
405
|
+
var buildExtractionPrompt = (messages) => {
|
|
406
|
+
const conversationText = messages.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
|
|
407
|
+
return `\uB2E4\uC74C \uB300\uD654\uC5D0\uC11C \uC0AC\uC6A9\uC790\uC5D0 \uB300\uD574 \uAE30\uC5B5\uD574\uB458 \uB9CC\uD55C \uC911\uC694\uD55C \uC815\uBCF4\uB97C \uCD94\uCD9C\uD574\uC8FC\uC138\uC694.
|
|
408
|
+
|
|
409
|
+
\uB300\uD654:
|
|
410
|
+
${conversationText}
|
|
411
|
+
|
|
412
|
+
\uB2E4\uC74C JSON \uD615\uC2DD\uC73C\uB85C\uB9CC \uC751\uB2F5\uD558\uC138\uC694 (JSON\uB9CC \uCD9C\uB825, \uB2E4\uB978 \uD14D\uC2A4\uD2B8 \uC5C6\uC774):
|
|
413
|
+
{
|
|
414
|
+
"extracted": [
|
|
415
|
+
{
|
|
416
|
+
"category": "basic|professional|preferences|context",
|
|
417
|
+
"key": "\uC815\uBCF4 \uD0A4 (\uC608: name, occupation, preferredLanguage)",
|
|
418
|
+
"value": "\uC815\uBCF4 \uAC12",
|
|
419
|
+
"confidence": 0.0-1.0,
|
|
420
|
+
"source": "\uADFC\uAC70\uAC00 \uB41C \uC6D0\uBB38 \uC77C\uBD80"
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
"shouldStore": true
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
\uCE74\uD14C\uACE0\uB9AC \uC124\uBA85:
|
|
427
|
+
- basic: \uC774\uB984, \uB098\uC774, \uC704\uCE58 \uB4F1 \uAE30\uBCF8 \uC815\uBCF4
|
|
428
|
+
- professional: \uC9C1\uC5C5, \uD68C\uC0AC, \uC804\uBB38 \uBD84\uC57C
|
|
429
|
+
- preferences: \uC120\uD638\uD558\uB294 \uC5B8\uC5B4, \uC2A4\uD0C0\uC77C, \uAD00\uC2EC\uC0AC
|
|
430
|
+
- context: \uD604\uC7AC \uD504\uB85C\uC81D\uD2B8, \uBAA9\uD45C, \uC9C4\uD589 \uC911\uC778 \uC791\uC5C5
|
|
431
|
+
|
|
432
|
+
\uADDC\uCE59:
|
|
433
|
+
1. \uBA85\uC2DC\uC801\uC73C\uB85C \uC5B8\uAE09\uB41C \uC815\uBCF4\uB9CC \uCD94\uCD9C (\uCD94\uB860 \uAE08\uC9C0)
|
|
434
|
+
2. \uAC1C\uC778\uC815\uBCF4 \uBBFC\uAC10\uB3C4\uAC00 \uB192\uC740 \uC815\uBCF4(\uC8FC\uBBFC\uBC88\uD638, \uBE44\uBC00\uBC88\uD638, \uCE74\uB4DC\uBC88\uD638 \uB4F1)\uB294 \uC81C\uC678
|
|
435
|
+
3. confidence\uB294 \uC815\uBCF4\uC758 \uD655\uC2E4\uC131 (0.8 \uC774\uC0C1 \uAD8C\uC7A5)
|
|
436
|
+
4. \uB300\uD654\uC5D0\uC11C \uC911\uC694 \uC815\uBCF4\uAC00 \uC5C6\uC73C\uBA74 extracted\uB97C \uBE48 \uBC30\uC5F4\uB85C \uBC18\uD658
|
|
437
|
+
5. key\uB294 \uC601\uBB38 camelCase\uB85C \uC791\uC131 (\uC608: userName, currentProject)`;
|
|
438
|
+
};
|
|
439
|
+
var parseExtractionResult = (text) => {
|
|
440
|
+
try {
|
|
441
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
442
|
+
if (!jsonMatch) {
|
|
443
|
+
return { extracted: [], shouldStore: false };
|
|
444
|
+
}
|
|
445
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
446
|
+
if (!parsed.extracted || !Array.isArray(parsed.extracted)) {
|
|
447
|
+
return { extracted: [], shouldStore: false };
|
|
448
|
+
}
|
|
449
|
+
const validExtracted = parsed.extracted.filter(
|
|
450
|
+
(item) => item.category && item.key && item.value !== void 0 && typeof item.confidence === "number"
|
|
451
|
+
);
|
|
452
|
+
return {
|
|
453
|
+
extracted: validExtracted,
|
|
454
|
+
shouldStore: parsed.shouldStore ?? validExtracted.length > 0
|
|
455
|
+
};
|
|
456
|
+
} catch {
|
|
457
|
+
return { extracted: [], shouldStore: false };
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function useInfoExtraction(options) {
|
|
461
|
+
const [isExtracting, setIsExtracting] = (0, import_react2.useState)(false);
|
|
462
|
+
const [lastExtraction, setLastExtraction] = (0, import_react2.useState)(null);
|
|
463
|
+
const { apiEndpoint, model, minConfidence = 0.8, globalMemory } = options;
|
|
464
|
+
const extractInfo = (0, import_react2.useCallback)(
|
|
465
|
+
async (messages) => {
|
|
466
|
+
if (messages.length === 0) {
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
setIsExtracting(true);
|
|
470
|
+
try {
|
|
471
|
+
const prompt = buildExtractionPrompt(messages);
|
|
472
|
+
const response = await fetch(apiEndpoint, {
|
|
473
|
+
method: "POST",
|
|
474
|
+
headers: { "Content-Type": "application/json" },
|
|
475
|
+
body: JSON.stringify({
|
|
476
|
+
messages: [{ role: "user", content: prompt }],
|
|
477
|
+
model: model || "default"
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
throw new Error(`API error: ${response.status}`);
|
|
482
|
+
}
|
|
483
|
+
const reader = response.body?.getReader();
|
|
484
|
+
if (!reader) {
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
const decoder = new TextDecoder();
|
|
488
|
+
let buffer = "";
|
|
489
|
+
let fullResponse = "";
|
|
490
|
+
while (true) {
|
|
491
|
+
const { done, value } = await reader.read();
|
|
492
|
+
if (done) break;
|
|
493
|
+
buffer += decoder.decode(value, { stream: true });
|
|
494
|
+
const lines = buffer.split("\n");
|
|
495
|
+
buffer = lines.pop() || "";
|
|
496
|
+
for (const line of lines) {
|
|
497
|
+
if (line.startsWith("data: ")) {
|
|
498
|
+
const data = line.slice(6);
|
|
499
|
+
if (data === "[DONE]") continue;
|
|
500
|
+
try {
|
|
501
|
+
const parsed = JSON.parse(data);
|
|
502
|
+
if (parsed.content) fullResponse += parsed.content;
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const result = parseExtractionResult(fullResponse);
|
|
509
|
+
const extracted = result.extracted.filter(
|
|
510
|
+
(item) => item.confidence >= minConfidence
|
|
511
|
+
);
|
|
512
|
+
if (result.shouldStore && globalMemory) {
|
|
513
|
+
for (const item of extracted) {
|
|
514
|
+
await globalMemory.set(`${item.category}.${item.key}`, item.value, {
|
|
515
|
+
category: item.category,
|
|
516
|
+
confidence: item.confidence,
|
|
517
|
+
source: item.source
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
setLastExtraction(extracted);
|
|
522
|
+
return extracted;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error("[useInfoExtraction] Failed to extract:", error);
|
|
525
|
+
return [];
|
|
526
|
+
} finally {
|
|
527
|
+
setIsExtracting(false);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
[apiEndpoint, model, minConfidence, globalMemory]
|
|
531
|
+
);
|
|
532
|
+
return {
|
|
533
|
+
extractInfo,
|
|
534
|
+
isExtracting,
|
|
535
|
+
lastExtraction
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
70
539
|
// src/react/hooks/useChatUI.ts
|
|
71
540
|
var DEFAULT_STORAGE_KEY = "chatllm_sessions";
|
|
72
541
|
var DEFAULT_COMPRESSION_THRESHOLD = 20;
|
|
73
542
|
var DEFAULT_KEEP_RECENT = 6;
|
|
543
|
+
var DEFAULT_RECOMPRESSION_THRESHOLD = 10;
|
|
544
|
+
var DEFAULT_TOKEN_LIMIT = 8e3;
|
|
74
545
|
var generateId = (prefix) => `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
75
546
|
var generateTitle = (messages) => {
|
|
76
547
|
const firstUserMessage = messages.find((m) => m.role === "user");
|
|
@@ -91,28 +562,50 @@ var useChatUI = (options) => {
|
|
|
91
562
|
keepRecentMessages = DEFAULT_KEEP_RECENT,
|
|
92
563
|
onSendMessage,
|
|
93
564
|
onSessionChange,
|
|
94
|
-
onError
|
|
565
|
+
onError,
|
|
566
|
+
// Memory options
|
|
567
|
+
useGlobalMemoryEnabled = true,
|
|
568
|
+
globalMemoryConfig,
|
|
569
|
+
enableAutoExtraction = true
|
|
95
570
|
} = options;
|
|
96
|
-
const [sessions, setSessions] = (0,
|
|
97
|
-
const [currentSessionId, setCurrentSessionId] = (0,
|
|
98
|
-
const [input, setInput] = (0,
|
|
99
|
-
const [isLoading, setIsLoading] = (0,
|
|
100
|
-
const [selectedModel, setSelectedModel] = (0,
|
|
101
|
-
const [sidebarOpen, setSidebarOpen] = (0,
|
|
102
|
-
const [settingsOpen, setSettingsOpen] = (0,
|
|
103
|
-
const [quotedText, setQuotedText] = (0,
|
|
104
|
-
const [selectedAction, setSelectedAction] = (0,
|
|
105
|
-
const [copiedMessageId, setCopiedMessageId] = (0,
|
|
106
|
-
const [editingMessageId, setEditingMessageId] = (0,
|
|
107
|
-
const [personalization, setPersonalization] = (0,
|
|
571
|
+
const [sessions, setSessions] = (0, import_react3.useState)([]);
|
|
572
|
+
const [currentSessionId, setCurrentSessionId] = (0, import_react3.useState)(null);
|
|
573
|
+
const [input, setInput] = (0, import_react3.useState)("");
|
|
574
|
+
const [isLoading, setIsLoading] = (0, import_react3.useState)(false);
|
|
575
|
+
const [selectedModel, setSelectedModel] = (0, import_react3.useState)(initialModel || models[0]?.id || "");
|
|
576
|
+
const [sidebarOpen, setSidebarOpen] = (0, import_react3.useState)(true);
|
|
577
|
+
const [settingsOpen, setSettingsOpen] = (0, import_react3.useState)(false);
|
|
578
|
+
const [quotedText, setQuotedText] = (0, import_react3.useState)(null);
|
|
579
|
+
const [selectedAction, setSelectedAction] = (0, import_react3.useState)(null);
|
|
580
|
+
const [copiedMessageId, setCopiedMessageId] = (0, import_react3.useState)(null);
|
|
581
|
+
const [editingMessageId, setEditingMessageId] = (0, import_react3.useState)(null);
|
|
582
|
+
const [personalization, setPersonalization] = (0, import_react3.useState)({
|
|
108
583
|
...DEFAULT_PERSONALIZATION,
|
|
109
584
|
...initialPersonalization
|
|
110
585
|
});
|
|
111
|
-
const [activeAlternatives, setActiveAlternatives] = (0,
|
|
112
|
-
const abortControllerRef = (0,
|
|
586
|
+
const [activeAlternatives, setActiveAlternatives] = (0, import_react3.useState)({});
|
|
587
|
+
const abortControllerRef = (0, import_react3.useRef)(null);
|
|
588
|
+
const memoryOptions = (0, import_react3.useMemo)(
|
|
589
|
+
() => ({
|
|
590
|
+
storageType: globalMemoryConfig?.storageType || "localStorage",
|
|
591
|
+
storageKey: globalMemoryConfig?.localStorage?.key || `${storageKey}_memory`,
|
|
592
|
+
apiEndpoint: globalMemoryConfig?.postgresql?.apiEndpoint,
|
|
593
|
+
userId: globalMemoryConfig?.postgresql?.userId,
|
|
594
|
+
authToken: globalMemoryConfig?.postgresql?.authToken
|
|
595
|
+
}),
|
|
596
|
+
[globalMemoryConfig, storageKey]
|
|
597
|
+
);
|
|
598
|
+
const globalMemory = useGlobalMemoryEnabled ? useGlobalMemory(memoryOptions) : null;
|
|
599
|
+
const infoExtraction = useInfoExtraction({
|
|
600
|
+
apiEndpoint,
|
|
601
|
+
model: selectedModel,
|
|
602
|
+
minConfidence: 0.8,
|
|
603
|
+
globalMemory
|
|
604
|
+
});
|
|
113
605
|
const currentSession = sessions.find((s) => s.id === currentSessionId) || null;
|
|
114
606
|
const messages = currentSession?.messages || [];
|
|
115
|
-
|
|
607
|
+
const compressionState = currentSession?.compressionState || null;
|
|
608
|
+
(0, import_react3.useEffect)(() => {
|
|
116
609
|
if (typeof window === "undefined") return;
|
|
117
610
|
const saved = localStorage.getItem(storageKey);
|
|
118
611
|
if (saved) {
|
|
@@ -134,20 +627,20 @@ var useChatUI = (options) => {
|
|
|
134
627
|
}
|
|
135
628
|
}
|
|
136
629
|
}, [storageKey]);
|
|
137
|
-
(0,
|
|
630
|
+
(0, import_react3.useEffect)(() => {
|
|
138
631
|
if (typeof window === "undefined") return;
|
|
139
632
|
if (sessions.length > 0) {
|
|
140
633
|
localStorage.setItem(storageKey, JSON.stringify(sessions));
|
|
141
634
|
}
|
|
142
635
|
}, [sessions, storageKey]);
|
|
143
|
-
(0,
|
|
636
|
+
(0, import_react3.useEffect)(() => {
|
|
144
637
|
if (typeof window === "undefined") return;
|
|
145
638
|
localStorage.setItem(`${storageKey}_personalization`, JSON.stringify(personalization));
|
|
146
639
|
}, [personalization, storageKey]);
|
|
147
|
-
(0,
|
|
640
|
+
(0, import_react3.useEffect)(() => {
|
|
148
641
|
onSessionChange?.(currentSession);
|
|
149
642
|
}, [currentSession, onSessionChange]);
|
|
150
|
-
const buildSystemPrompt = (0,
|
|
643
|
+
const buildSystemPrompt = (0, import_react3.useCallback)(() => {
|
|
151
644
|
const parts = [];
|
|
152
645
|
const { userProfile, responseStyle, language } = personalization;
|
|
153
646
|
if (userProfile.nickname) {
|
|
@@ -175,9 +668,16 @@ var useChatUI = (options) => {
|
|
|
175
668
|
const languageNames = { ko: "\uD55C\uAD6D\uC5B4", en: "\uC601\uC5B4", ja: "\uC77C\uBCF8\uC5B4" };
|
|
176
669
|
parts.push(`\uC751\uB2F5 \uC5B8\uC5B4: ${languageNames[language] || language}\uB85C \uC751\uB2F5\uD574\uC8FC\uC138\uC694.`);
|
|
177
670
|
}
|
|
671
|
+
if (useGlobalMemoryEnabled && globalMemory && personalization.useMemory) {
|
|
672
|
+
const memoryContext = globalMemory.toPromptContext();
|
|
673
|
+
if (memoryContext) {
|
|
674
|
+
parts.push("");
|
|
675
|
+
parts.push(memoryContext);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
178
678
|
return parts.length > 0 ? parts.join("\n") : "";
|
|
179
|
-
}, [personalization]);
|
|
180
|
-
const compressContext = (0,
|
|
679
|
+
}, [personalization, globalMemory, useGlobalMemoryEnabled]);
|
|
680
|
+
const compressContext = (0, import_react3.useCallback)(async (messagesToCompress, model) => {
|
|
181
681
|
const conversationText = messagesToCompress.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
|
|
182
682
|
const summaryPrompt = `\uB2E4\uC74C \uB300\uD654 \uB0B4\uC6A9\uC744 \uD575\uC2EC \uC815\uBCF4\uB9CC \uC720\uC9C0\uD558\uBA74\uC11C \uAC04\uACB0\uD558\uAC8C \uC694\uC57D\uD574\uC8FC\uC138\uC694.
|
|
183
683
|
\uC911\uC694\uD55C \uACB0\uC815\uC0AC\uD56D, \uC0AC\uC6A9\uC790 \uC694\uAD6C\uC0AC\uD56D, \uB9E5\uB77D \uC815\uBCF4\uB97C \uBCF4\uC874\uD558\uC138\uC694.
|
|
@@ -224,7 +724,68 @@ ${conversationText}
|
|
|
224
724
|
return "";
|
|
225
725
|
}
|
|
226
726
|
}, [apiEndpoint]);
|
|
227
|
-
const
|
|
727
|
+
const incrementalCompressContext = (0, import_react3.useCallback)(
|
|
728
|
+
async (existingSummary, newMessages, model) => {
|
|
729
|
+
const newConversation = newMessages.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
|
|
730
|
+
const mergePrompt = `\uAE30\uC874 \uB300\uD654 \uC694\uC57D\uACFC \uC0C8\uB85C\uC6B4 \uB300\uD654 \uB0B4\uC6A9\uC744 \uD1B5\uD569\uD558\uC5EC \uD558\uB098\uC758 \uC694\uC57D\uC73C\uB85C \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694.
|
|
731
|
+
|
|
732
|
+
[\uAE30\uC874 \uC694\uC57D]
|
|
733
|
+
${existingSummary}
|
|
734
|
+
|
|
735
|
+
[\uC0C8\uB85C\uC6B4 \uB300\uD654]
|
|
736
|
+
${newConversation}
|
|
737
|
+
|
|
738
|
+
\uD1B5\uD569 \uC694\uC57D \uC791\uC131 \uC2DC \uB2E4\uC74C\uC744 \uC9C0\uCF1C\uC8FC\uC138\uC694:
|
|
739
|
+
1. \uD575\uC2EC \uACB0\uC815\uC0AC\uD56D, \uC0AC\uC6A9\uC790 \uC694\uAD6C\uC0AC\uD56D, \uB9E5\uB77D \uC815\uBCF4 \uBCF4\uC874
|
|
740
|
+
2. \uC911\uBCF5 \uC815\uBCF4 \uC81C\uAC70
|
|
741
|
+
3. \uC2DC\uAC04\uC21C \uD750\uB984 \uC720\uC9C0
|
|
742
|
+
4. 300\uC790 \uC774\uB0B4\uB85C \uAC04\uACB0\uD558\uAC8C
|
|
743
|
+
|
|
744
|
+
\uD1B5\uD569 \uC694\uC57D:`;
|
|
745
|
+
try {
|
|
746
|
+
const response = await fetch(apiEndpoint, {
|
|
747
|
+
method: "POST",
|
|
748
|
+
headers: { "Content-Type": "application/json" },
|
|
749
|
+
body: JSON.stringify({
|
|
750
|
+
messages: [{ role: "user", content: mergePrompt }],
|
|
751
|
+
model
|
|
752
|
+
})
|
|
753
|
+
});
|
|
754
|
+
if (!response.ok) return existingSummary;
|
|
755
|
+
const reader = response.body?.getReader();
|
|
756
|
+
if (!reader) return existingSummary;
|
|
757
|
+
const decoder = new TextDecoder();
|
|
758
|
+
let buffer = "";
|
|
759
|
+
let summary = "";
|
|
760
|
+
while (true) {
|
|
761
|
+
const { done, value } = await reader.read();
|
|
762
|
+
if (done) break;
|
|
763
|
+
buffer += decoder.decode(value, { stream: true });
|
|
764
|
+
const lines = buffer.split("\n");
|
|
765
|
+
buffer = lines.pop() || "";
|
|
766
|
+
for (const line of lines) {
|
|
767
|
+
if (line.startsWith("data: ")) {
|
|
768
|
+
const data = line.slice(6);
|
|
769
|
+
if (data === "[DONE]") continue;
|
|
770
|
+
try {
|
|
771
|
+
const parsed = JSON.parse(data);
|
|
772
|
+
if (parsed.content) summary += parsed.content;
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return summary || existingSummary;
|
|
779
|
+
} catch {
|
|
780
|
+
return existingSummary;
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
[apiEndpoint]
|
|
784
|
+
);
|
|
785
|
+
const estimateTokens = (0, import_react3.useCallback)((messages2) => {
|
|
786
|
+
return messages2.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0);
|
|
787
|
+
}, []);
|
|
788
|
+
const newSession = (0, import_react3.useCallback)(() => {
|
|
228
789
|
const now = Date.now();
|
|
229
790
|
const newSess = {
|
|
230
791
|
id: generateId("session"),
|
|
@@ -237,14 +798,14 @@ ${conversationText}
|
|
|
237
798
|
setSessions((prev) => [newSess, ...prev]);
|
|
238
799
|
setCurrentSessionId(newSess.id);
|
|
239
800
|
}, [selectedModel]);
|
|
240
|
-
const selectSession = (0,
|
|
801
|
+
const selectSession = (0, import_react3.useCallback)((id) => {
|
|
241
802
|
const session = sessions.find((s) => s.id === id);
|
|
242
803
|
if (session) {
|
|
243
804
|
setCurrentSessionId(id);
|
|
244
805
|
setSelectedModel(session.model);
|
|
245
806
|
}
|
|
246
807
|
}, [sessions]);
|
|
247
|
-
const deleteSession = (0,
|
|
808
|
+
const deleteSession = (0, import_react3.useCallback)((id) => {
|
|
248
809
|
setSessions((prev) => {
|
|
249
810
|
const filtered = prev.filter((s) => s.id !== id);
|
|
250
811
|
if (currentSessionId === id) {
|
|
@@ -256,7 +817,7 @@ ${conversationText}
|
|
|
256
817
|
return filtered;
|
|
257
818
|
});
|
|
258
819
|
}, [currentSessionId, storageKey]);
|
|
259
|
-
const setModel = (0,
|
|
820
|
+
const setModel = (0, import_react3.useCallback)((model) => {
|
|
260
821
|
setSelectedModel(model);
|
|
261
822
|
if (currentSessionId) {
|
|
262
823
|
setSessions(
|
|
@@ -264,10 +825,10 @@ ${conversationText}
|
|
|
264
825
|
);
|
|
265
826
|
}
|
|
266
827
|
}, [currentSessionId]);
|
|
267
|
-
const toggleSidebar = (0,
|
|
268
|
-
const openSettings = (0,
|
|
269
|
-
const closeSettings = (0,
|
|
270
|
-
const copyMessage = (0,
|
|
828
|
+
const toggleSidebar = (0, import_react3.useCallback)(() => setSidebarOpen((prev) => !prev), []);
|
|
829
|
+
const openSettings = (0, import_react3.useCallback)(() => setSettingsOpen(true), []);
|
|
830
|
+
const closeSettings = (0, import_react3.useCallback)(() => setSettingsOpen(false), []);
|
|
831
|
+
const copyMessage = (0, import_react3.useCallback)((content, id) => {
|
|
271
832
|
if (typeof navigator !== "undefined") {
|
|
272
833
|
navigator.clipboard.writeText(content).then(() => {
|
|
273
834
|
setCopiedMessageId(id);
|
|
@@ -275,21 +836,21 @@ ${conversationText}
|
|
|
275
836
|
});
|
|
276
837
|
}
|
|
277
838
|
}, []);
|
|
278
|
-
const startEdit = (0,
|
|
839
|
+
const startEdit = (0, import_react3.useCallback)((message) => {
|
|
279
840
|
if (message.role === "user") {
|
|
280
841
|
setEditingMessageId(message.id);
|
|
281
842
|
}
|
|
282
843
|
}, []);
|
|
283
|
-
const cancelEdit = (0,
|
|
844
|
+
const cancelEdit = (0, import_react3.useCallback)(() => {
|
|
284
845
|
setEditingMessageId(null);
|
|
285
846
|
}, []);
|
|
286
|
-
const stopGeneration = (0,
|
|
847
|
+
const stopGeneration = (0, import_react3.useCallback)(() => {
|
|
287
848
|
abortControllerRef.current?.abort();
|
|
288
849
|
}, []);
|
|
289
|
-
const updatePersonalization = (0,
|
|
850
|
+
const updatePersonalization = (0, import_react3.useCallback)((config) => {
|
|
290
851
|
setPersonalization((prev) => ({ ...prev, ...config }));
|
|
291
852
|
}, []);
|
|
292
|
-
const sendMessage = (0,
|
|
853
|
+
const sendMessage = (0, import_react3.useCallback)(async (content) => {
|
|
293
854
|
const messageContent = content || input;
|
|
294
855
|
if (!messageContent.trim() || isLoading) return;
|
|
295
856
|
let sessionId = currentSessionId;
|
|
@@ -355,17 +916,50 @@ ${finalContent}`;
|
|
|
355
916
|
const session = sessions.find((s) => s.id === capturedSessionId);
|
|
356
917
|
const existingMessages = session?.messages || [];
|
|
357
918
|
let messagesToSend = [...existingMessages, userMessage];
|
|
358
|
-
|
|
359
|
-
|
|
919
|
+
const recompressionThreshold = DEFAULT_RECOMPRESSION_THRESHOLD;
|
|
920
|
+
const tokenLimit = DEFAULT_TOKEN_LIMIT;
|
|
921
|
+
let contextSummary = session?.compressionState?.contextSummary || session?.contextSummary;
|
|
922
|
+
const summaryAfterIndex = session?.compressionState?.summaryAfterIndex || session?.summaryAfterIndex || 0;
|
|
923
|
+
let compressionCount = session?.compressionState?.compressionCount || 0;
|
|
924
|
+
const newMessageCount = messagesToSend.length - summaryAfterIndex;
|
|
925
|
+
const estimatedTokensCount = estimateTokens(messagesToSend);
|
|
926
|
+
const needsInitialCompression = !contextSummary && messagesToSend.length > contextCompressionThreshold;
|
|
927
|
+
const needsRecompression = contextSummary && (newMessageCount > recompressionThreshold || estimatedTokensCount > tokenLimit * 0.7);
|
|
928
|
+
if (needsInitialCompression || needsRecompression) {
|
|
360
929
|
const toCompress = messagesToSend.slice(0, -keepRecentMessages);
|
|
361
|
-
|
|
930
|
+
let summary;
|
|
931
|
+
if (needsRecompression && contextSummary) {
|
|
932
|
+
const newMessages = toCompress.slice(summaryAfterIndex);
|
|
933
|
+
summary = await incrementalCompressContext(contextSummary, newMessages, selectedModel);
|
|
934
|
+
} else {
|
|
935
|
+
summary = await compressContext(toCompress, selectedModel);
|
|
936
|
+
}
|
|
362
937
|
if (summary) {
|
|
363
938
|
contextSummary = summary;
|
|
939
|
+
compressionCount += 1;
|
|
364
940
|
setSessions(
|
|
365
941
|
(prev) => prev.map(
|
|
366
|
-
(s) => s.id === capturedSessionId ? {
|
|
942
|
+
(s) => s.id === capturedSessionId ? {
|
|
943
|
+
...s,
|
|
944
|
+
contextSummary: summary,
|
|
945
|
+
summaryAfterIndex: toCompress.length,
|
|
946
|
+
compressionState: {
|
|
947
|
+
contextSummary: summary,
|
|
948
|
+
summaryAfterIndex: toCompress.length,
|
|
949
|
+
compressionCount,
|
|
950
|
+
lastCompressionAt: Date.now()
|
|
951
|
+
}
|
|
952
|
+
} : s
|
|
367
953
|
)
|
|
368
954
|
);
|
|
955
|
+
if (enableAutoExtraction && globalMemory) {
|
|
956
|
+
const messagesForExtraction = toCompress.map((m) => ({
|
|
957
|
+
role: m.role,
|
|
958
|
+
content: m.content
|
|
959
|
+
}));
|
|
960
|
+
infoExtraction.extractInfo(messagesForExtraction).catch(() => {
|
|
961
|
+
});
|
|
962
|
+
}
|
|
369
963
|
}
|
|
370
964
|
}
|
|
371
965
|
let chatMessages;
|
|
@@ -517,7 +1111,7 @@ ${contextSummary}` },
|
|
|
517
1111
|
onSendMessage,
|
|
518
1112
|
onError
|
|
519
1113
|
]);
|
|
520
|
-
const saveEdit = (0,
|
|
1114
|
+
const saveEdit = (0, import_react3.useCallback)(async (content) => {
|
|
521
1115
|
if (!editingMessageId || !currentSession || !currentSessionId) return;
|
|
522
1116
|
const messageIndex = currentSession.messages.findIndex((m) => m.id === editingMessageId);
|
|
523
1117
|
if (messageIndex === -1) return;
|
|
@@ -534,7 +1128,7 @@ ${contextSummary}` },
|
|
|
534
1128
|
setEditingMessageId(null);
|
|
535
1129
|
await sendMessage(content);
|
|
536
1130
|
}, [editingMessageId, currentSession, currentSessionId, sendMessage]);
|
|
537
|
-
const regenerate = (0,
|
|
1131
|
+
const regenerate = (0, import_react3.useCallback)(async (messageId) => {
|
|
538
1132
|
if (!currentSession || !currentSessionId || isLoading) return;
|
|
539
1133
|
const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
|
|
540
1134
|
if (assistantIndex === -1) return;
|
|
@@ -556,7 +1150,7 @@ ${contextSummary}` },
|
|
|
556
1150
|
setInput(userMessage.content);
|
|
557
1151
|
await sendMessage(userMessage.content);
|
|
558
1152
|
}, [currentSession, currentSessionId, isLoading, sendMessage]);
|
|
559
|
-
const askOtherModel = (0,
|
|
1153
|
+
const askOtherModel = (0, import_react3.useCallback)(async (messageId, targetModel) => {
|
|
560
1154
|
if (!currentSession || !currentSessionId || isLoading) return;
|
|
561
1155
|
const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
|
|
562
1156
|
if (assistantIndex === -1) return;
|
|
@@ -708,13 +1302,13 @@ ${currentSession.contextSummary}` },
|
|
|
708
1302
|
onSendMessage,
|
|
709
1303
|
onError
|
|
710
1304
|
]);
|
|
711
|
-
const setActiveAlternative = (0,
|
|
1305
|
+
const setActiveAlternative = (0, import_react3.useCallback)((assistantMessageId, index) => {
|
|
712
1306
|
setActiveAlternatives((prev) => ({
|
|
713
1307
|
...prev,
|
|
714
1308
|
[assistantMessageId]: index
|
|
715
1309
|
}));
|
|
716
1310
|
}, []);
|
|
717
|
-
const getActiveAlternative = (0,
|
|
1311
|
+
const getActiveAlternative = (0, import_react3.useCallback)((assistantMessageId) => {
|
|
718
1312
|
return activeAlternatives[assistantMessageId] ?? 0;
|
|
719
1313
|
}, [activeAlternatives]);
|
|
720
1314
|
return {
|
|
@@ -756,7 +1350,18 @@ ${currentSession.contextSummary}` },
|
|
|
756
1350
|
setActiveAlternative,
|
|
757
1351
|
getActiveAlternative,
|
|
758
1352
|
updatePersonalization,
|
|
759
|
-
models
|
|
1353
|
+
models,
|
|
1354
|
+
// Memory
|
|
1355
|
+
globalMemory,
|
|
1356
|
+
compressionState,
|
|
1357
|
+
extractUserInfo: async () => {
|
|
1358
|
+
if (!enableAutoExtraction || !globalMemory) return;
|
|
1359
|
+
const recentMessages = messages.slice(-10).map((m) => ({
|
|
1360
|
+
role: m.role,
|
|
1361
|
+
content: m.content
|
|
1362
|
+
}));
|
|
1363
|
+
await infoExtraction.extractInfo(recentMessages);
|
|
1364
|
+
}
|
|
760
1365
|
};
|
|
761
1366
|
};
|
|
762
1367
|
|
|
@@ -1064,7 +1669,7 @@ var ChatSidebar = ({
|
|
|
1064
1669
|
};
|
|
1065
1670
|
|
|
1066
1671
|
// src/react/components/ChatHeader.tsx
|
|
1067
|
-
var
|
|
1672
|
+
var import_react4 = require("react");
|
|
1068
1673
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1069
1674
|
var ChatHeader = ({
|
|
1070
1675
|
title,
|
|
@@ -1077,7 +1682,7 @@ var ChatHeader = ({
|
|
|
1077
1682
|
showModelSelector = true,
|
|
1078
1683
|
showSettings = true
|
|
1079
1684
|
}) => {
|
|
1080
|
-
const [modelDropdownOpen, setModelDropdownOpen] = (0,
|
|
1685
|
+
const [modelDropdownOpen, setModelDropdownOpen] = (0, import_react4.useState)(false);
|
|
1081
1686
|
const currentModel = models.find((m) => m.id === model);
|
|
1082
1687
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1083
1688
|
"header",
|
|
@@ -1331,7 +1936,7 @@ var ChatHeader = ({
|
|
|
1331
1936
|
};
|
|
1332
1937
|
|
|
1333
1938
|
// src/react/components/ChatInput.tsx
|
|
1334
|
-
var
|
|
1939
|
+
var import_react5 = require("react");
|
|
1335
1940
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1336
1941
|
var ChatInput = ({
|
|
1337
1942
|
value,
|
|
@@ -1347,9 +1952,9 @@ var ChatInput = ({
|
|
|
1347
1952
|
onActionSelect,
|
|
1348
1953
|
actions = []
|
|
1349
1954
|
}) => {
|
|
1350
|
-
const textareaRef = (0,
|
|
1351
|
-
const [actionMenuOpen, setActionMenuOpen] = (0,
|
|
1352
|
-
(0,
|
|
1955
|
+
const textareaRef = (0, import_react5.useRef)(null);
|
|
1956
|
+
const [actionMenuOpen, setActionMenuOpen] = (0, import_react5.useState)(false);
|
|
1957
|
+
(0, import_react5.useEffect)(() => {
|
|
1353
1958
|
if (textareaRef.current) {
|
|
1354
1959
|
textareaRef.current.style.height = "auto";
|
|
1355
1960
|
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
|
|
@@ -1647,16 +2252,16 @@ var ChatInput = ({
|
|
|
1647
2252
|
};
|
|
1648
2253
|
|
|
1649
2254
|
// src/react/components/MessageList.tsx
|
|
1650
|
-
var
|
|
2255
|
+
var import_react9 = require("react");
|
|
1651
2256
|
|
|
1652
2257
|
// src/react/components/MessageBubble.tsx
|
|
1653
|
-
var
|
|
2258
|
+
var import_react8 = require("react");
|
|
1654
2259
|
|
|
1655
2260
|
// src/react/components/MarkdownRenderer.tsx
|
|
1656
|
-
var
|
|
2261
|
+
var import_react7 = __toESM(require("react"));
|
|
1657
2262
|
|
|
1658
2263
|
// src/react/components/LinkChip.tsx
|
|
1659
|
-
var
|
|
2264
|
+
var import_react6 = require("react");
|
|
1660
2265
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1661
2266
|
var getDomain = (url) => {
|
|
1662
2267
|
try {
|
|
@@ -1710,7 +2315,7 @@ var LinkChip = ({
|
|
|
1710
2315
|
index,
|
|
1711
2316
|
style
|
|
1712
2317
|
}) => {
|
|
1713
|
-
const [isHovered, setIsHovered] = (0,
|
|
2318
|
+
const [isHovered, setIsHovered] = (0, import_react6.useState)(false);
|
|
1714
2319
|
const domain = getDomain(url);
|
|
1715
2320
|
const shortName = getShortName(domain);
|
|
1716
2321
|
const domainColor = getDomainColor(domain);
|
|
@@ -1824,6 +2429,8 @@ var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
|
1824
2429
|
var IMAGE_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
1825
2430
|
var LINK_REGEX = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
|
|
1826
2431
|
var SOURCE_LINKS_REGEX = /(\*{0,2}출처:?\*{0,2}\s*)?((?:(?<!!)\[`?[^\]]+`?\]\([^)]+\)\s*)+)/gi;
|
|
2432
|
+
var INLINE_CHOICE_PATTERN = /\[(\d+)\]\s*([^\[\n]+?)(?=\s*\[\d+\]|$)/g;
|
|
2433
|
+
var INLINE_CHOICE_LINE_PATTERN = /^(.+?)?\s*(\[\d+\][^\[]+(?:\[\d+\][^\[]+)+)$/;
|
|
1827
2434
|
var CODE_BLOCK_REGEX = /```(\w*)\n?([\s\S]*?)```/g;
|
|
1828
2435
|
var INLINE_CODE_REGEX = /`([^`]+)`/g;
|
|
1829
2436
|
var BOLD_REGEX = /\*\*([^*]+)\*\*/g;
|
|
@@ -1843,6 +2450,30 @@ var parseSourceLinks = (text) => {
|
|
|
1843
2450
|
}
|
|
1844
2451
|
return links;
|
|
1845
2452
|
};
|
|
2453
|
+
var parseInlineChoices = (text) => {
|
|
2454
|
+
const lineMatch = text.match(INLINE_CHOICE_LINE_PATTERN);
|
|
2455
|
+
if (!lineMatch) return null;
|
|
2456
|
+
const title = lineMatch[1]?.trim();
|
|
2457
|
+
const choicesText = lineMatch[2];
|
|
2458
|
+
const choices = [];
|
|
2459
|
+
let match;
|
|
2460
|
+
const regex = new RegExp(INLINE_CHOICE_PATTERN.source, "g");
|
|
2461
|
+
while ((match = regex.exec(choicesText)) !== null) {
|
|
2462
|
+
const number = parseInt(match[1]);
|
|
2463
|
+
const choiceText = match[2].trim();
|
|
2464
|
+
if (choiceText) {
|
|
2465
|
+
choices.push({
|
|
2466
|
+
number,
|
|
2467
|
+
text: choiceText,
|
|
2468
|
+
fullText: `[${number}] ${choiceText}`
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (choices.length >= 2) {
|
|
2473
|
+
return { title, choices };
|
|
2474
|
+
}
|
|
2475
|
+
return null;
|
|
2476
|
+
};
|
|
1846
2477
|
var parseInlineElements = (text, key) => {
|
|
1847
2478
|
const elements = [];
|
|
1848
2479
|
let lastIndex = 0;
|
|
@@ -1979,7 +2610,7 @@ var MarkdownTable = ({ data }) => {
|
|
|
1979
2610
|
);
|
|
1980
2611
|
};
|
|
1981
2612
|
var CodeBlock = ({ language, code }) => {
|
|
1982
|
-
const [copied, setCopied] =
|
|
2613
|
+
const [copied, setCopied] = import_react7.default.useState(false);
|
|
1983
2614
|
const handleCopy = async () => {
|
|
1984
2615
|
try {
|
|
1985
2616
|
await navigator.clipboard.writeText(code);
|
|
@@ -2062,9 +2693,9 @@ var CodeBlock = ({ language, code }) => {
|
|
|
2062
2693
|
);
|
|
2063
2694
|
};
|
|
2064
2695
|
var ImageWithCopyButton = ({ src, alt, imageKey }) => {
|
|
2065
|
-
const [isHovered, setIsHovered] =
|
|
2066
|
-
const [copyState, setCopyState] =
|
|
2067
|
-
const imgRef =
|
|
2696
|
+
const [isHovered, setIsHovered] = import_react7.default.useState(false);
|
|
2697
|
+
const [copyState, setCopyState] = import_react7.default.useState("idle");
|
|
2698
|
+
const imgRef = import_react7.default.useRef(null);
|
|
2068
2699
|
const getImageBlob = async () => {
|
|
2069
2700
|
const img = imgRef.current;
|
|
2070
2701
|
if (img && img.complete && img.naturalWidth > 0) {
|
|
@@ -2279,8 +2910,8 @@ var ImageWithCopyButton = ({ src, alt, imageKey }) => {
|
|
|
2279
2910
|
imageKey
|
|
2280
2911
|
);
|
|
2281
2912
|
};
|
|
2282
|
-
var ChoiceButtons = ({ choices, onChoiceClick }) => {
|
|
2283
|
-
const [hoveredIndex, setHoveredIndex] =
|
|
2913
|
+
var ChoiceButtons = ({ choices, title, onChoiceClick }) => {
|
|
2914
|
+
const [hoveredIndex, setHoveredIndex] = import_react7.default.useState(null);
|
|
2284
2915
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
2285
2916
|
"div",
|
|
2286
2917
|
{
|
|
@@ -2300,14 +2931,14 @@ var ChoiceButtons = ({ choices, onChoiceClick }) => {
|
|
|
2300
2931
|
"div",
|
|
2301
2932
|
{
|
|
2302
2933
|
style: {
|
|
2303
|
-
fontSize: "12px",
|
|
2934
|
+
fontSize: title ? "14px" : "12px",
|
|
2304
2935
|
fontWeight: 600,
|
|
2305
|
-
color: "var(--chatllm-text-muted, #6b7280)",
|
|
2936
|
+
color: title ? "var(--chatllm-text, #374151)" : "var(--chatllm-text-muted, #6b7280)",
|
|
2306
2937
|
marginBottom: "4px",
|
|
2307
|
-
textTransform: "uppercase",
|
|
2308
|
-
letterSpacing: "0.5px"
|
|
2938
|
+
textTransform: title ? "none" : "uppercase",
|
|
2939
|
+
letterSpacing: title ? "normal" : "0.5px"
|
|
2309
2940
|
},
|
|
2310
|
-
children: "\uC120\uD0DD\uD558\uC138\uC694"
|
|
2941
|
+
children: title || "\uC120\uD0DD\uD558\uC138\uC694"
|
|
2311
2942
|
}
|
|
2312
2943
|
),
|
|
2313
2944
|
choices.map((choice, index) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
@@ -2403,7 +3034,7 @@ var SourceLinksSection = ({ links, label }) => {
|
|
|
2403
3034
|
);
|
|
2404
3035
|
};
|
|
2405
3036
|
var MarkdownRenderer = ({ content, className, onChoiceClick }) => {
|
|
2406
|
-
const rendered = (0,
|
|
3037
|
+
const rendered = (0, import_react7.useMemo)(() => {
|
|
2407
3038
|
const elements = [];
|
|
2408
3039
|
let processedContent = content;
|
|
2409
3040
|
const codeBlocks = [];
|
|
@@ -2501,7 +3132,7 @@ var MarkdownRenderer = ({ content, className, onChoiceClick }) => {
|
|
|
2501
3132
|
borderRadius: "0 8px 8px 0",
|
|
2502
3133
|
color: "var(--chatllm-text, #374151)"
|
|
2503
3134
|
},
|
|
2504
|
-
children: blockquoteLines.map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
3135
|
+
children: blockquoteLines.map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react7.default.Fragment, { children: [
|
|
2505
3136
|
parseInlineElements(line, `bq-line-${i}`),
|
|
2506
3137
|
i < blockquoteLines.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("br", {})
|
|
2507
3138
|
] }, i))
|
|
@@ -2637,6 +3268,23 @@ var MarkdownRenderer = ({ content, className, onChoiceClick }) => {
|
|
|
2637
3268
|
elements.push(/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("br", {}, `br-${lineIndex}`));
|
|
2638
3269
|
return;
|
|
2639
3270
|
}
|
|
3271
|
+
if (onChoiceClick) {
|
|
3272
|
+
const choiceBlock = parseInlineChoices(line);
|
|
3273
|
+
if (choiceBlock) {
|
|
3274
|
+
elements.push(
|
|
3275
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
3276
|
+
ChoiceButtons,
|
|
3277
|
+
{
|
|
3278
|
+
choices: choiceBlock.choices,
|
|
3279
|
+
title: choiceBlock.title,
|
|
3280
|
+
onChoiceClick
|
|
3281
|
+
},
|
|
3282
|
+
`inline-choices-${lineIndex}`
|
|
3283
|
+
)
|
|
3284
|
+
);
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
2640
3288
|
elements.push(
|
|
2641
3289
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: { margin: "4px 0" }, children: parseInlineElements(line, `p-${lineIndex}`) }, `p-${lineIndex}`)
|
|
2642
3290
|
);
|
|
@@ -2679,8 +3327,8 @@ var MessageBubble = ({
|
|
|
2679
3327
|
nextAssistantMessage,
|
|
2680
3328
|
onChoiceClick
|
|
2681
3329
|
}) => {
|
|
2682
|
-
const [showActions, setShowActions] = (0,
|
|
2683
|
-
const [showModelMenu, setShowModelMenu] = (0,
|
|
3330
|
+
const [showActions, setShowActions] = (0, import_react8.useState)(false);
|
|
3331
|
+
const [showModelMenu, setShowModelMenu] = (0, import_react8.useState)(false);
|
|
2684
3332
|
const isUser = message.role === "user";
|
|
2685
3333
|
const isAssistant = message.role === "assistant";
|
|
2686
3334
|
const relevantAlternatives = isUser ? alternatives : message.alternatives;
|
|
@@ -3073,14 +3721,14 @@ var MessageList = ({
|
|
|
3073
3721
|
editingId,
|
|
3074
3722
|
onChoiceClick
|
|
3075
3723
|
}) => {
|
|
3076
|
-
const messagesEndRef = (0,
|
|
3077
|
-
const containerRef = (0,
|
|
3078
|
-
const [selectedText, setSelectedText] = (0,
|
|
3079
|
-
const [selectionPosition, setSelectionPosition] = (0,
|
|
3080
|
-
(0,
|
|
3724
|
+
const messagesEndRef = (0, import_react9.useRef)(null);
|
|
3725
|
+
const containerRef = (0, import_react9.useRef)(null);
|
|
3726
|
+
const [selectedText, setSelectedText] = (0, import_react9.useState)("");
|
|
3727
|
+
const [selectionPosition, setSelectionPosition] = (0, import_react9.useState)(null);
|
|
3728
|
+
(0, import_react9.useEffect)(() => {
|
|
3081
3729
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
3082
3730
|
}, [messages]);
|
|
3083
|
-
const handleMouseUp = (0,
|
|
3731
|
+
const handleMouseUp = (0, import_react9.useCallback)(() => {
|
|
3084
3732
|
const selection = window.getSelection();
|
|
3085
3733
|
const text = selection?.toString().trim();
|
|
3086
3734
|
if (text && text.length > 0) {
|
|
@@ -3457,7 +4105,7 @@ var EmptyState = ({
|
|
|
3457
4105
|
};
|
|
3458
4106
|
|
|
3459
4107
|
// src/react/components/MemoryPanel.tsx
|
|
3460
|
-
var
|
|
4108
|
+
var import_react10 = require("react");
|
|
3461
4109
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
3462
4110
|
var categoryLabels = {
|
|
3463
4111
|
context: "\uB300\uD654 \uCEE8\uD14D\uC2A4\uD2B8",
|
|
@@ -3479,8 +4127,8 @@ var MemoryPanel = ({
|
|
|
3479
4127
|
isOpen,
|
|
3480
4128
|
onToggle
|
|
3481
4129
|
}) => {
|
|
3482
|
-
const [expandedId, setExpandedId] = (0,
|
|
3483
|
-
const [activeTab, setActiveTab] = (0,
|
|
4130
|
+
const [expandedId, setExpandedId] = (0, import_react10.useState)(null);
|
|
4131
|
+
const [activeTab, setActiveTab] = (0, import_react10.useState)("all");
|
|
3484
4132
|
const filteredItems = activeTab === "all" ? items : items.filter((item) => item.category === activeTab);
|
|
3485
4133
|
const formatDate2 = (timestamp) => {
|
|
3486
4134
|
const date = new Date(timestamp);
|
|
@@ -3798,7 +4446,7 @@ var MemoryPanel = ({
|
|
|
3798
4446
|
};
|
|
3799
4447
|
|
|
3800
4448
|
// src/react/components/SettingsModal.tsx
|
|
3801
|
-
var
|
|
4449
|
+
var import_react11 = require("react");
|
|
3802
4450
|
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
3803
4451
|
var DEFAULT_PERSONALIZATION2 = {
|
|
3804
4452
|
responseStyle: {
|
|
@@ -3823,8 +4471,8 @@ var SettingsModal = ({
|
|
|
3823
4471
|
apiKeyLabel = "API Key",
|
|
3824
4472
|
apiKeyDescription = "Cloud \uBAA8\uB378 \uC0AC\uC6A9\uC5D0 \uD544\uC694\uD569\uB2C8\uB2E4"
|
|
3825
4473
|
}) => {
|
|
3826
|
-
const [activeTab, setActiveTab] = (0,
|
|
3827
|
-
const [localApiKey, setLocalApiKey] = (0,
|
|
4474
|
+
const [activeTab, setActiveTab] = (0, import_react11.useState)("general");
|
|
4475
|
+
const [localApiKey, setLocalApiKey] = (0, import_react11.useState)(apiKey);
|
|
3828
4476
|
if (!isOpen) return null;
|
|
3829
4477
|
const updateResponseStyle = (key, value) => {
|
|
3830
4478
|
onPersonalizationChange({
|
|
@@ -4432,7 +5080,7 @@ var ChatUI = ({
|
|
|
4432
5080
|
onSessionChange,
|
|
4433
5081
|
onError
|
|
4434
5082
|
}) => {
|
|
4435
|
-
|
|
5083
|
+
import_react12.default.useEffect(() => {
|
|
4436
5084
|
injectStyles();
|
|
4437
5085
|
}, []);
|
|
4438
5086
|
const hookOptions = {
|
|
@@ -4500,8 +5148,8 @@ var ChatUI = ({
|
|
|
4500
5148
|
const handleChoiceClick = (choice) => {
|
|
4501
5149
|
setInput(choice.text);
|
|
4502
5150
|
};
|
|
4503
|
-
const [memoryPanelOpen, setMemoryPanelOpen] = (0,
|
|
4504
|
-
const memoryItems =
|
|
5151
|
+
const [memoryPanelOpen, setMemoryPanelOpen] = (0, import_react12.useState)(false);
|
|
5152
|
+
const memoryItems = import_react12.default.useMemo(() => {
|
|
4505
5153
|
const items = [];
|
|
4506
5154
|
if (currentSession?.contextSummary) {
|
|
4507
5155
|
items.push({
|
|
@@ -4683,4 +5331,22 @@ var ChatUI = ({
|
|
|
4683
5331
|
SettingsModal,
|
|
4684
5332
|
useChatUI
|
|
4685
5333
|
});
|
|
5334
|
+
/**
|
|
5335
|
+
* @description localStorage 기반 메모리 저장소 어댑터
|
|
5336
|
+
* @license MIT
|
|
5337
|
+
*/
|
|
5338
|
+
/**
|
|
5339
|
+
* @description PostgreSQL REST API 기반 메모리 저장소 어댑터
|
|
5340
|
+
* @license MIT
|
|
5341
|
+
*/
|
|
5342
|
+
/**
|
|
5343
|
+
* @description 글로벌 메모리 관리 훅
|
|
5344
|
+
* localStorage 또는 PostgreSQL API를 통해 세션 간 정보 유지
|
|
5345
|
+
* @license MIT
|
|
5346
|
+
*/
|
|
5347
|
+
/**
|
|
5348
|
+
* @description 자동 정보 추출 훅
|
|
5349
|
+
* LLM을 사용하여 대화에서 중요 정보를 자동으로 추출
|
|
5350
|
+
* @license MIT
|
|
5351
|
+
*/
|
|
4686
5352
|
//# sourceMappingURL=index.js.map
|