@lvce-editor/chat-view 1.17.0 → 1.19.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/chatViewWorkerMain.js +424 -79
- package/package.json +1 -1
|
@@ -1192,6 +1192,9 @@ const getComposerWidth = width => {
|
|
|
1192
1192
|
const getMinComposerHeight = lineHeight => {
|
|
1193
1193
|
return lineHeight + 8;
|
|
1194
1194
|
};
|
|
1195
|
+
const getMaxComposerHeight = (lineHeight, maxComposerRows) => {
|
|
1196
|
+
return lineHeight * Math.max(1, maxComposerRows) + 8;
|
|
1197
|
+
};
|
|
1195
1198
|
const estimateComposerHeight = (value, lineHeight) => {
|
|
1196
1199
|
const lineCount = value.split('\n').length;
|
|
1197
1200
|
return lineCount * lineHeight + 8;
|
|
@@ -1200,17 +1203,19 @@ const getComposerHeight = async (state, value, width = state.width) => {
|
|
|
1200
1203
|
const {
|
|
1201
1204
|
composerFontFamily,
|
|
1202
1205
|
composerFontSize,
|
|
1203
|
-
composerLineHeight
|
|
1206
|
+
composerLineHeight,
|
|
1207
|
+
maxComposerRows
|
|
1204
1208
|
} = state;
|
|
1205
1209
|
const minimumHeight = getMinComposerHeight(composerLineHeight);
|
|
1210
|
+
const maximumHeight = getMaxComposerHeight(composerLineHeight, maxComposerRows);
|
|
1206
1211
|
const content = value || ' ';
|
|
1207
1212
|
const composerWidth = getComposerWidth(width);
|
|
1208
1213
|
try {
|
|
1209
1214
|
const measuredHeight = await measureTextBlockHeight(content, composerFontFamily, composerFontSize, composerLineHeight, composerWidth);
|
|
1210
1215
|
const height = Math.ceil(measuredHeight) + 8;
|
|
1211
|
-
return Math.max(minimumHeight, height);
|
|
1216
|
+
return Math.max(minimumHeight, Math.min(maximumHeight, height));
|
|
1212
1217
|
} catch {
|
|
1213
|
-
return Math.max(minimumHeight, estimateComposerHeight(value, composerLineHeight));
|
|
1218
|
+
return Math.max(minimumHeight, Math.min(maximumHeight, estimateComposerHeight(value, composerLineHeight)));
|
|
1214
1219
|
}
|
|
1215
1220
|
};
|
|
1216
1221
|
const getMinComposerHeightForState = state => {
|
|
@@ -1385,6 +1390,7 @@ const createDefaultState = () => {
|
|
|
1385
1390
|
const composerLineHeight = 20;
|
|
1386
1391
|
return {
|
|
1387
1392
|
assetDir: '',
|
|
1393
|
+
chatListScrollTop: 0,
|
|
1388
1394
|
composerFontFamily: 'system-ui',
|
|
1389
1395
|
composerFontSize,
|
|
1390
1396
|
composerHeight: composerLineHeight + 8,
|
|
@@ -1399,6 +1405,8 @@ const createDefaultState = () => {
|
|
|
1399
1405
|
inputSource: 'script',
|
|
1400
1406
|
lastSubmittedSessionId: '',
|
|
1401
1407
|
listItemHeight: 40,
|
|
1408
|
+
maxComposerRows: 5,
|
|
1409
|
+
messagesScrollTop: 0,
|
|
1402
1410
|
mockApiCommandId: '',
|
|
1403
1411
|
models: getDefaultModels(),
|
|
1404
1412
|
nextMessageId: 1,
|
|
@@ -1420,6 +1428,7 @@ const createDefaultState = () => {
|
|
|
1420
1428
|
messages: [],
|
|
1421
1429
|
title: defaultSessionTitle()
|
|
1422
1430
|
}],
|
|
1431
|
+
streamingEnabled: false,
|
|
1423
1432
|
tokensMax: 0,
|
|
1424
1433
|
tokensUsed: 0,
|
|
1425
1434
|
uid: 0,
|
|
@@ -1457,7 +1466,7 @@ const create = (uid, x, y, width, height, platform, assetDir) => {
|
|
|
1457
1466
|
};
|
|
1458
1467
|
|
|
1459
1468
|
const isEqual$1 = (oldState, newState) => {
|
|
1460
|
-
return oldState.initial === newState.initial;
|
|
1469
|
+
return oldState.initial === newState.initial && oldState.composerHeight === newState.composerHeight && oldState.composerLineHeight === newState.composerLineHeight && oldState.composerFontFamily === newState.composerFontFamily && oldState.composerFontSize === newState.composerFontSize;
|
|
1461
1470
|
};
|
|
1462
1471
|
|
|
1463
1472
|
const diffFocus = (oldState, newState) => {
|
|
@@ -1471,12 +1480,17 @@ const isEqual = (oldState, newState) => {
|
|
|
1471
1480
|
return oldState.composerValue === newState.composerValue && oldState.initial === newState.initial && oldState.renamingSessionId === newState.renamingSessionId && oldState.selectedModelId === newState.selectedModelId && oldState.selectedSessionId === newState.selectedSessionId && oldState.sessions === newState.sessions && oldState.tokensMax === newState.tokensMax && oldState.tokensUsed === newState.tokensUsed && oldState.usageOverviewEnabled === newState.usageOverviewEnabled && oldState.viewMode === newState.viewMode;
|
|
1472
1481
|
};
|
|
1473
1482
|
|
|
1483
|
+
const diffScrollTop = (oldState, newState) => {
|
|
1484
|
+
return oldState.chatListScrollTop === newState.chatListScrollTop && oldState.messagesScrollTop === newState.messagesScrollTop;
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1474
1487
|
const RenderItems = 4;
|
|
1475
1488
|
const RenderFocus = 6;
|
|
1476
1489
|
const RenderFocusContext = 7;
|
|
1477
1490
|
const RenderValue = 8;
|
|
1478
1491
|
const RenderCss = 10;
|
|
1479
1492
|
const RenderIncremental = 11;
|
|
1493
|
+
const RenderScrollTop = 12;
|
|
1480
1494
|
|
|
1481
1495
|
const diffValue = (oldState, newState) => {
|
|
1482
1496
|
if (oldState.composerValue === newState.composerValue) {
|
|
@@ -1485,8 +1499,8 @@ const diffValue = (oldState, newState) => {
|
|
|
1485
1499
|
return newState.inputSource !== 'script';
|
|
1486
1500
|
};
|
|
1487
1501
|
|
|
1488
|
-
const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus];
|
|
1489
|
-
const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext];
|
|
1502
|
+
const modules = [isEqual, diffValue, diffFocus, isEqual$1, diffFocus, diffScrollTop];
|
|
1503
|
+
const numbers = [RenderIncremental, RenderValue, RenderFocus, RenderCss, RenderFocusContext, RenderScrollTop];
|
|
1490
1504
|
|
|
1491
1505
|
const diff = (oldState, newState) => {
|
|
1492
1506
|
const diffResult = [];
|
|
@@ -1519,6 +1533,7 @@ const FocusSelector = 'Viewlet.focusSelector';
|
|
|
1519
1533
|
const SetCss = 'Viewlet.setCss';
|
|
1520
1534
|
const SetDom2 = 'Viewlet.setDom2';
|
|
1521
1535
|
const SetFocusContext = 'Viewlet.setFocusContext';
|
|
1536
|
+
const SetProperty = 'Viewlet.setProperty';
|
|
1522
1537
|
const SetValueByName = 'Viewlet.setValueByName';
|
|
1523
1538
|
const SetPatches = 'Viewlet.setPatches';
|
|
1524
1539
|
|
|
@@ -2495,7 +2510,16 @@ const executeChatTool = async (name, rawArguments, options) => {
|
|
|
2495
2510
|
});
|
|
2496
2511
|
};
|
|
2497
2512
|
|
|
2498
|
-
const
|
|
2513
|
+
const getClientRequestIdHeader = () => {
|
|
2514
|
+
return {
|
|
2515
|
+
'x-client-request-id': crypto.randomUUID()
|
|
2516
|
+
};
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
const getOpenApiApiEndpoint = (openApiApiBaseUrl, stream) => {
|
|
2520
|
+
if (stream) {
|
|
2521
|
+
return `${openApiApiBaseUrl}/chat/completions?stream=true`;
|
|
2522
|
+
}
|
|
2499
2523
|
return `${openApiApiBaseUrl}/chat/completions`;
|
|
2500
2524
|
};
|
|
2501
2525
|
|
|
@@ -2520,6 +2544,148 @@ const getTextContent = content => {
|
|
|
2520
2544
|
return textParts.join('\n');
|
|
2521
2545
|
};
|
|
2522
2546
|
|
|
2547
|
+
const getStreamChunkText = content => {
|
|
2548
|
+
if (typeof content === 'string') {
|
|
2549
|
+
return content;
|
|
2550
|
+
}
|
|
2551
|
+
if (!Array.isArray(content)) {
|
|
2552
|
+
return '';
|
|
2553
|
+
}
|
|
2554
|
+
return content.map(part => {
|
|
2555
|
+
if (!part || typeof part !== 'object') {
|
|
2556
|
+
return '';
|
|
2557
|
+
}
|
|
2558
|
+
const text = Reflect.get(part, 'text');
|
|
2559
|
+
return typeof text === 'string' ? text : '';
|
|
2560
|
+
}).join('');
|
|
2561
|
+
};
|
|
2562
|
+
const parseSseEvent = eventChunk => {
|
|
2563
|
+
const lines = eventChunk.split('\n');
|
|
2564
|
+
const dataLines = [];
|
|
2565
|
+
for (const line of lines) {
|
|
2566
|
+
if (!line.startsWith('data:')) {
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
dataLines.push(line.slice(5).trimStart());
|
|
2570
|
+
}
|
|
2571
|
+
return dataLines;
|
|
2572
|
+
};
|
|
2573
|
+
const parseOpenApiStream = async (response, onTextChunk) => {
|
|
2574
|
+
if (!response.body) {
|
|
2575
|
+
return {
|
|
2576
|
+
details: 'request-failed',
|
|
2577
|
+
type: 'error'
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
const reader = response.body.getReader();
|
|
2581
|
+
const decoder = new TextDecoder();
|
|
2582
|
+
let remainder = '';
|
|
2583
|
+
let text = '';
|
|
2584
|
+
let done = false;
|
|
2585
|
+
while (!done) {
|
|
2586
|
+
const {
|
|
2587
|
+
done: streamDone,
|
|
2588
|
+
value
|
|
2589
|
+
} = await reader.read();
|
|
2590
|
+
if (streamDone) {
|
|
2591
|
+
done = true;
|
|
2592
|
+
} else if (value) {
|
|
2593
|
+
remainder += decoder.decode(value, {
|
|
2594
|
+
stream: true
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
while (true) {
|
|
2598
|
+
const separatorIndex = remainder.indexOf('\n\n');
|
|
2599
|
+
if (separatorIndex === -1) {
|
|
2600
|
+
break;
|
|
2601
|
+
}
|
|
2602
|
+
const rawEvent = remainder.slice(0, separatorIndex);
|
|
2603
|
+
remainder = remainder.slice(separatorIndex + 2);
|
|
2604
|
+
const dataLines = parseSseEvent(rawEvent);
|
|
2605
|
+
if (dataLines.length === 0) {
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
for (const line of dataLines) {
|
|
2609
|
+
if (line === '[DONE]') {
|
|
2610
|
+
done = true;
|
|
2611
|
+
break;
|
|
2612
|
+
}
|
|
2613
|
+
let parsed;
|
|
2614
|
+
try {
|
|
2615
|
+
parsed = JSON.parse(line);
|
|
2616
|
+
} catch {
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
2620
|
+
continue;
|
|
2621
|
+
}
|
|
2622
|
+
const choices = Reflect.get(parsed, 'choices');
|
|
2623
|
+
if (!Array.isArray(choices)) {
|
|
2624
|
+
continue;
|
|
2625
|
+
}
|
|
2626
|
+
const firstChoice = choices[0];
|
|
2627
|
+
if (!firstChoice || typeof firstChoice !== 'object') {
|
|
2628
|
+
continue;
|
|
2629
|
+
}
|
|
2630
|
+
const delta = Reflect.get(firstChoice, 'delta');
|
|
2631
|
+
if (!delta || typeof delta !== 'object') {
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
const content = Reflect.get(delta, 'content');
|
|
2635
|
+
const chunkText = getStreamChunkText(content);
|
|
2636
|
+
if (!chunkText) {
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
text += chunkText;
|
|
2640
|
+
if (onTextChunk) {
|
|
2641
|
+
await onTextChunk(chunkText);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (remainder) {
|
|
2647
|
+
const dataLines = parseSseEvent(remainder);
|
|
2648
|
+
for (const line of dataLines) {
|
|
2649
|
+
if (line === '[DONE]') {
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
let parsed;
|
|
2653
|
+
try {
|
|
2654
|
+
parsed = JSON.parse(line);
|
|
2655
|
+
} catch {
|
|
2656
|
+
continue;
|
|
2657
|
+
}
|
|
2658
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
const choices = Reflect.get(parsed, 'choices');
|
|
2662
|
+
if (!Array.isArray(choices)) {
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
const firstChoice = choices[0];
|
|
2666
|
+
if (!firstChoice || typeof firstChoice !== 'object') {
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
const delta = Reflect.get(firstChoice, 'delta');
|
|
2670
|
+
if (!delta || typeof delta !== 'object') {
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
const content = Reflect.get(delta, 'content');
|
|
2674
|
+
const chunkText = getStreamChunkText(content);
|
|
2675
|
+
if (!chunkText) {
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
text += chunkText;
|
|
2679
|
+
if (onTextChunk) {
|
|
2680
|
+
await onTextChunk(chunkText);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
return {
|
|
2685
|
+
text,
|
|
2686
|
+
type: 'success'
|
|
2687
|
+
};
|
|
2688
|
+
};
|
|
2523
2689
|
const getOpenApiErrorDetails = async response => {
|
|
2524
2690
|
let parsed;
|
|
2525
2691
|
try {
|
|
@@ -2543,7 +2709,13 @@ const getOpenApiErrorDetails = async response => {
|
|
|
2543
2709
|
errorType: typeof errorType === 'string' ? errorType : undefined
|
|
2544
2710
|
};
|
|
2545
2711
|
};
|
|
2546
|
-
const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform) => {
|
|
2712
|
+
const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApiApiBaseUrl, assetDir, platform, options) => {
|
|
2713
|
+
const {
|
|
2714
|
+
onTextChunk,
|
|
2715
|
+
stream
|
|
2716
|
+
} = options ?? {
|
|
2717
|
+
stream: false
|
|
2718
|
+
};
|
|
2547
2719
|
const completionMessages = messages.map(message => ({
|
|
2548
2720
|
content: message.text,
|
|
2549
2721
|
role: message.role
|
|
@@ -2553,16 +2725,20 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
|
|
|
2553
2725
|
for (let i = 0; i <= maxToolIterations; i++) {
|
|
2554
2726
|
let response;
|
|
2555
2727
|
try {
|
|
2556
|
-
response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl), {
|
|
2728
|
+
response = await fetch(getOpenApiApiEndpoint(openApiApiBaseUrl, stream), {
|
|
2557
2729
|
body: JSON.stringify({
|
|
2558
2730
|
messages: completionMessages,
|
|
2559
2731
|
model: modelId,
|
|
2732
|
+
...(stream ? {
|
|
2733
|
+
stream: true
|
|
2734
|
+
} : {}),
|
|
2560
2735
|
tool_choice: 'auto',
|
|
2561
2736
|
tools
|
|
2562
2737
|
}),
|
|
2563
2738
|
headers: {
|
|
2564
2739
|
Authorization: `Bearer ${openApiApiKey}`,
|
|
2565
|
-
'Content-Type': 'application/json'
|
|
2740
|
+
'Content-Type': 'application/json',
|
|
2741
|
+
...getClientRequestIdHeader()
|
|
2566
2742
|
},
|
|
2567
2743
|
method: 'POST'
|
|
2568
2744
|
});
|
|
@@ -2587,6 +2763,9 @@ const getOpenApiAssistantText = async (messages, modelId, openApiApiKey, openApi
|
|
|
2587
2763
|
type: 'error'
|
|
2588
2764
|
};
|
|
2589
2765
|
}
|
|
2766
|
+
if (stream) {
|
|
2767
|
+
return parseOpenApiStream(response, onTextChunk);
|
|
2768
|
+
}
|
|
2590
2769
|
let parsed;
|
|
2591
2770
|
try {
|
|
2592
2771
|
parsed = await response.json();
|
|
@@ -2758,7 +2937,8 @@ const getOpenRouterLimitInfo = async (openRouterApiKey, openRouterApiBaseUrl) =>
|
|
|
2758
2937
|
try {
|
|
2759
2938
|
response = await fetch(getOpenRouterKeyEndpoint(openRouterApiBaseUrl), {
|
|
2760
2939
|
headers: {
|
|
2761
|
-
Authorization: `Bearer ${openRouterApiKey}
|
|
2940
|
+
Authorization: `Bearer ${openRouterApiKey}`,
|
|
2941
|
+
...getClientRequestIdHeader()
|
|
2762
2942
|
},
|
|
2763
2943
|
method: 'GET'
|
|
2764
2944
|
});
|
|
@@ -2816,7 +2996,8 @@ const getOpenRouterAssistantText = async (messages, modelId, openRouterApiKey, o
|
|
|
2816
2996
|
}),
|
|
2817
2997
|
headers: {
|
|
2818
2998
|
Authorization: `Bearer ${openRouterApiKey}`,
|
|
2819
|
-
'Content-Type': 'application/json'
|
|
2999
|
+
'Content-Type': 'application/json',
|
|
3000
|
+
...getClientRequestIdHeader()
|
|
2820
3001
|
},
|
|
2821
3002
|
method: 'POST'
|
|
2822
3003
|
});
|
|
@@ -2965,7 +3146,7 @@ const getOpenRouterErrorMessage = errorResult => {
|
|
|
2965
3146
|
}
|
|
2966
3147
|
};
|
|
2967
3148
|
|
|
2968
|
-
|
|
3149
|
+
// cspell:ignore openrouter
|
|
2969
3150
|
const getOpenRouterModelId = selectedModelId => {
|
|
2970
3151
|
const openRouterPrefix = 'openrouter/';
|
|
2971
3152
|
if (selectedModelId.toLowerCase().startsWith(openRouterPrefix)) {
|
|
@@ -2984,7 +3165,7 @@ const isOpenApiModel = (selectedModelId, models) => {
|
|
|
2984
3165
|
return normalizedModelId.startsWith('openapi/') || normalizedModelId.startsWith('openai/');
|
|
2985
3166
|
};
|
|
2986
3167
|
|
|
2987
|
-
|
|
3168
|
+
// cspell:ignore openrouter
|
|
2988
3169
|
|
|
2989
3170
|
const isOpenRouterModel = (selectedModelId, models) => {
|
|
2990
3171
|
const selectedModel = models.find(model => model.id === selectedModelId);
|
|
@@ -3001,12 +3182,14 @@ const getAiResponse = async ({
|
|
|
3001
3182
|
mockApiCommandId,
|
|
3002
3183
|
models,
|
|
3003
3184
|
nextMessageId,
|
|
3185
|
+
onTextChunk,
|
|
3004
3186
|
openApiApiBaseUrl,
|
|
3005
3187
|
openApiApiKey,
|
|
3006
3188
|
openRouterApiBaseUrl,
|
|
3007
3189
|
openRouterApiKey,
|
|
3008
3190
|
platform,
|
|
3009
3191
|
selectedModelId,
|
|
3192
|
+
streamingEnabled = false,
|
|
3010
3193
|
useMockApi,
|
|
3011
3194
|
userText
|
|
3012
3195
|
}) => {
|
|
@@ -3015,7 +3198,10 @@ const getAiResponse = async ({
|
|
|
3015
3198
|
const usesOpenRouterModel = isOpenRouterModel(selectedModelId, models);
|
|
3016
3199
|
if (usesOpenApiModel) {
|
|
3017
3200
|
if (openApiApiKey) {
|
|
3018
|
-
const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform
|
|
3201
|
+
const result = await getOpenApiAssistantText(messages, getOpenApiModelId(selectedModelId), openApiApiKey, openApiApiBaseUrl, assetDir, platform, {
|
|
3202
|
+
onTextChunk,
|
|
3203
|
+
stream: streamingEnabled
|
|
3204
|
+
});
|
|
3019
3205
|
if (result.type === 'success') {
|
|
3020
3206
|
const {
|
|
3021
3207
|
text: assistantText
|
|
@@ -3233,6 +3419,37 @@ const focusInput = state => {
|
|
|
3233
3419
|
};
|
|
3234
3420
|
};
|
|
3235
3421
|
|
|
3422
|
+
const appendMessageToSelectedSession = (sessions, selectedSessionId, message) => {
|
|
3423
|
+
return sessions.map(session => {
|
|
3424
|
+
if (session.id !== selectedSessionId) {
|
|
3425
|
+
return session;
|
|
3426
|
+
}
|
|
3427
|
+
return {
|
|
3428
|
+
...session,
|
|
3429
|
+
messages: [...session.messages, message]
|
|
3430
|
+
};
|
|
3431
|
+
});
|
|
3432
|
+
};
|
|
3433
|
+
const updateMessageTextInSelectedSession = (sessions, selectedSessionId, messageId, text, inProgress) => {
|
|
3434
|
+
return sessions.map(session => {
|
|
3435
|
+
if (session.id !== selectedSessionId) {
|
|
3436
|
+
return session;
|
|
3437
|
+
}
|
|
3438
|
+
return {
|
|
3439
|
+
...session,
|
|
3440
|
+
messages: session.messages.map(message => {
|
|
3441
|
+
if (message.id !== messageId) {
|
|
3442
|
+
return message;
|
|
3443
|
+
}
|
|
3444
|
+
return {
|
|
3445
|
+
...message,
|
|
3446
|
+
inProgress,
|
|
3447
|
+
text
|
|
3448
|
+
};
|
|
3449
|
+
})
|
|
3450
|
+
};
|
|
3451
|
+
});
|
|
3452
|
+
};
|
|
3236
3453
|
const handleSubmit = async state => {
|
|
3237
3454
|
const {
|
|
3238
3455
|
assetDir,
|
|
@@ -3248,6 +3465,7 @@ const handleSubmit = async state => {
|
|
|
3248
3465
|
selectedModelId,
|
|
3249
3466
|
selectedSessionId,
|
|
3250
3467
|
sessions,
|
|
3468
|
+
streamingEnabled,
|
|
3251
3469
|
useMockApi,
|
|
3252
3470
|
viewMode
|
|
3253
3471
|
} = state;
|
|
@@ -3265,6 +3483,18 @@ const handleSubmit = async state => {
|
|
|
3265
3483
|
text: userText,
|
|
3266
3484
|
time: userTime
|
|
3267
3485
|
};
|
|
3486
|
+
const assistantMessageId = `message-${nextMessageId + 1}`;
|
|
3487
|
+
const assistantTime = new Date().toLocaleTimeString([], {
|
|
3488
|
+
hour: '2-digit',
|
|
3489
|
+
minute: '2-digit'
|
|
3490
|
+
});
|
|
3491
|
+
const inProgressAssistantMessage = {
|
|
3492
|
+
id: assistantMessageId,
|
|
3493
|
+
inProgress: true,
|
|
3494
|
+
role: 'assistant',
|
|
3495
|
+
text: '',
|
|
3496
|
+
time: assistantTime
|
|
3497
|
+
};
|
|
3268
3498
|
let workingSessions = sessions;
|
|
3269
3499
|
if (viewMode === 'detail') {
|
|
3270
3500
|
const loadedSession = await getChatSession(selectedSessionId);
|
|
@@ -3282,7 +3512,7 @@ const handleSubmit = async state => {
|
|
|
3282
3512
|
const newSessionId = generateSessionId();
|
|
3283
3513
|
const newSession = {
|
|
3284
3514
|
id: newSessionId,
|
|
3285
|
-
messages: [userMessage],
|
|
3515
|
+
messages: streamingEnabled ? [userMessage, inProgressAssistantMessage] : [userMessage],
|
|
3286
3516
|
title: `Chat ${workingSessions.length + 1}`
|
|
3287
3517
|
};
|
|
3288
3518
|
await saveChatSession(newSession);
|
|
@@ -3298,15 +3528,8 @@ const handleSubmit = async state => {
|
|
|
3298
3528
|
viewMode: 'detail'
|
|
3299
3529
|
});
|
|
3300
3530
|
} else {
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3303
|
-
return session;
|
|
3304
|
-
}
|
|
3305
|
-
return {
|
|
3306
|
-
...session,
|
|
3307
|
-
messages: [...session.messages, userMessage]
|
|
3308
|
-
};
|
|
3309
|
-
});
|
|
3531
|
+
const updatedWithUser = appendMessageToSelectedSession(workingSessions, selectedSessionId, userMessage);
|
|
3532
|
+
const updatedSessions = streamingEnabled ? appendMessageToSelectedSession(updatedWithUser, selectedSessionId, inProgressAssistantMessage) : updatedWithUser;
|
|
3310
3533
|
const selectedSession = updatedSessions.find(session => session.id === selectedSessionId);
|
|
3311
3534
|
if (selectedSession) {
|
|
3312
3535
|
await saveChatSession(selectedSession);
|
|
@@ -3324,39 +3547,56 @@ const handleSubmit = async state => {
|
|
|
3324
3547
|
set(state.uid, state, optimisticState);
|
|
3325
3548
|
// @ts-ignore
|
|
3326
3549
|
await invoke('Chat.rerender');
|
|
3327
|
-
|
|
3328
|
-
|
|
3550
|
+
let latestState = optimisticState;
|
|
3551
|
+
let previousState = optimisticState;
|
|
3552
|
+
const selectedOptimisticSession = latestState.sessions.find(session => session.id === latestState.selectedSessionId);
|
|
3553
|
+
const messages = (selectedOptimisticSession?.messages ?? []).filter(message => !message.inProgress);
|
|
3554
|
+
const onTextChunk = streamingEnabled ? async chunk => {
|
|
3555
|
+
const selectedSession = latestState.sessions.find(session => session.id === latestState.selectedSessionId);
|
|
3556
|
+
if (!selectedSession) {
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
const assistantMessage = selectedSession.messages.find(message => message.id === assistantMessageId);
|
|
3560
|
+
if (!assistantMessage) {
|
|
3561
|
+
return;
|
|
3562
|
+
}
|
|
3563
|
+
const updatedText = assistantMessage.text + chunk;
|
|
3564
|
+
const updatedSessions = updateMessageTextInSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessageId, updatedText, true);
|
|
3565
|
+
const nextState = {
|
|
3566
|
+
...latestState,
|
|
3567
|
+
sessions: updatedSessions
|
|
3568
|
+
};
|
|
3569
|
+
set(state.uid, previousState, nextState);
|
|
3570
|
+
previousState = nextState;
|
|
3571
|
+
latestState = nextState;
|
|
3572
|
+
// @ts-ignore
|
|
3573
|
+
await invoke('Chat.rerender');
|
|
3574
|
+
} : undefined;
|
|
3329
3575
|
const assistantMessage = await getAiResponse({
|
|
3330
3576
|
assetDir,
|
|
3331
3577
|
messages,
|
|
3332
3578
|
mockApiCommandId,
|
|
3333
3579
|
models,
|
|
3334
3580
|
nextMessageId: optimisticState.nextMessageId,
|
|
3581
|
+
onTextChunk,
|
|
3335
3582
|
openApiApiBaseUrl,
|
|
3336
3583
|
openApiApiKey,
|
|
3337
3584
|
openRouterApiBaseUrl,
|
|
3338
3585
|
openRouterApiKey,
|
|
3339
3586
|
platform,
|
|
3340
3587
|
selectedModelId,
|
|
3588
|
+
streamingEnabled,
|
|
3341
3589
|
useMockApi,
|
|
3342
3590
|
userText
|
|
3343
3591
|
});
|
|
3344
|
-
const updatedSessions =
|
|
3345
|
-
|
|
3346
|
-
return session;
|
|
3347
|
-
}
|
|
3348
|
-
return {
|
|
3349
|
-
...session,
|
|
3350
|
-
messages: [...session.messages, assistantMessage]
|
|
3351
|
-
};
|
|
3352
|
-
});
|
|
3353
|
-
const selectedSession = updatedSessions.find(session => session.id === optimisticState.selectedSessionId);
|
|
3592
|
+
const updatedSessions = streamingEnabled ? updateMessageTextInSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessageId, assistantMessage.text, false) : appendMessageToSelectedSession(latestState.sessions, latestState.selectedSessionId, assistantMessage);
|
|
3593
|
+
const selectedSession = updatedSessions.find(session => session.id === latestState.selectedSessionId);
|
|
3354
3594
|
if (selectedSession) {
|
|
3355
3595
|
await saveChatSession(selectedSession);
|
|
3356
3596
|
}
|
|
3357
3597
|
return focusInput({
|
|
3358
|
-
...
|
|
3359
|
-
nextMessageId:
|
|
3598
|
+
...latestState,
|
|
3599
|
+
nextMessageId: latestState.nextMessageId + 1,
|
|
3360
3600
|
sessions: updatedSessions
|
|
3361
3601
|
});
|
|
3362
3602
|
};
|
|
@@ -3405,7 +3645,7 @@ const OpenApiApiKeyInput = 'open-api-api-key';
|
|
|
3405
3645
|
const SaveOpenApiApiKey = 'save-openapi-api-key';
|
|
3406
3646
|
const OpenOpenApiApiKeySettings = 'open-openapi-api-key-settings';
|
|
3407
3647
|
|
|
3408
|
-
|
|
3648
|
+
// cspell:ignore openrouter
|
|
3409
3649
|
const OpenRouterApiKeyInput = 'open-router-api-key';
|
|
3410
3650
|
const SaveOpenRouterApiKey = 'save-openrouter-api-key';
|
|
3411
3651
|
const OpenOpenRouterApiKeySettings = 'open-openrouter-api-key-settings';
|
|
@@ -3652,7 +3892,29 @@ const handleModelChange = async (state, value) => {
|
|
|
3652
3892
|
};
|
|
3653
3893
|
|
|
3654
3894
|
const handleNewline = async state => {
|
|
3655
|
-
|
|
3895
|
+
const {
|
|
3896
|
+
composerValue
|
|
3897
|
+
} = state;
|
|
3898
|
+
return handleInput(state, Composer, `${composerValue}\n`);
|
|
3899
|
+
};
|
|
3900
|
+
|
|
3901
|
+
const handleChatListScroll = async (state, chatListScrollTop) => {
|
|
3902
|
+
if (state.chatListScrollTop === chatListScrollTop) {
|
|
3903
|
+
return state;
|
|
3904
|
+
}
|
|
3905
|
+
return {
|
|
3906
|
+
...state,
|
|
3907
|
+
chatListScrollTop
|
|
3908
|
+
};
|
|
3909
|
+
};
|
|
3910
|
+
const handleMessagesScroll = async (state, messagesScrollTop) => {
|
|
3911
|
+
if (state.messagesScrollTop === messagesScrollTop) {
|
|
3912
|
+
return state;
|
|
3913
|
+
}
|
|
3914
|
+
return {
|
|
3915
|
+
...state,
|
|
3916
|
+
messagesScrollTop
|
|
3917
|
+
};
|
|
3656
3918
|
};
|
|
3657
3919
|
|
|
3658
3920
|
const id = 7201;
|
|
@@ -3681,6 +3943,32 @@ const isObject = value => {
|
|
|
3681
3943
|
return typeof value === 'object' && value !== null;
|
|
3682
3944
|
};
|
|
3683
3945
|
|
|
3946
|
+
const getSavedChatListScrollTop = savedState => {
|
|
3947
|
+
if (!isObject(savedState)) {
|
|
3948
|
+
return undefined;
|
|
3949
|
+
}
|
|
3950
|
+
const {
|
|
3951
|
+
chatListScrollTop
|
|
3952
|
+
} = savedState;
|
|
3953
|
+
if (typeof chatListScrollTop !== 'number') {
|
|
3954
|
+
return undefined;
|
|
3955
|
+
}
|
|
3956
|
+
return chatListScrollTop;
|
|
3957
|
+
};
|
|
3958
|
+
|
|
3959
|
+
const getSavedMessagesScrollTop = savedState => {
|
|
3960
|
+
if (!isObject(savedState)) {
|
|
3961
|
+
return undefined;
|
|
3962
|
+
}
|
|
3963
|
+
const {
|
|
3964
|
+
messagesScrollTop
|
|
3965
|
+
} = savedState;
|
|
3966
|
+
if (typeof messagesScrollTop !== 'number') {
|
|
3967
|
+
return undefined;
|
|
3968
|
+
}
|
|
3969
|
+
return messagesScrollTop;
|
|
3970
|
+
};
|
|
3971
|
+
|
|
3684
3972
|
const getSavedSelectedModelId = savedState => {
|
|
3685
3973
|
if (!isObject(savedState)) {
|
|
3686
3974
|
return undefined;
|
|
@@ -3733,6 +4021,39 @@ const getSavedViewMode = savedState => {
|
|
|
3733
4021
|
return viewMode;
|
|
3734
4022
|
};
|
|
3735
4023
|
|
|
4024
|
+
const loadOpenApiApiKey = async () => {
|
|
4025
|
+
try {
|
|
4026
|
+
const savedOpenApiKey = await get('secrets.openApiKey');
|
|
4027
|
+
if (typeof savedOpenApiKey === 'string' && savedOpenApiKey) {
|
|
4028
|
+
return savedOpenApiKey;
|
|
4029
|
+
}
|
|
4030
|
+
const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
|
|
4031
|
+
if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
|
|
4032
|
+
return legacySavedOpenApiApiKey;
|
|
4033
|
+
}
|
|
4034
|
+
const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
|
|
4035
|
+
return typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
|
|
4036
|
+
} catch {
|
|
4037
|
+
return '';
|
|
4038
|
+
}
|
|
4039
|
+
};
|
|
4040
|
+
const loadOpenRouterApiKey = async () => {
|
|
4041
|
+
try {
|
|
4042
|
+
const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
|
|
4043
|
+
return typeof savedOpenRouterApiKey === 'string' ? savedOpenRouterApiKey : '';
|
|
4044
|
+
} catch {
|
|
4045
|
+
return '';
|
|
4046
|
+
}
|
|
4047
|
+
};
|
|
4048
|
+
const loadPreferences = async () => {
|
|
4049
|
+
const openApiApiKey = await loadOpenApiApiKey();
|
|
4050
|
+
const openRouterApiKey = await loadOpenRouterApiKey();
|
|
4051
|
+
return {
|
|
4052
|
+
openApiApiKey,
|
|
4053
|
+
openRouterApiKey
|
|
4054
|
+
};
|
|
4055
|
+
};
|
|
4056
|
+
|
|
3736
4057
|
const toSummarySession = session => {
|
|
3737
4058
|
return {
|
|
3738
4059
|
id: session.id,
|
|
@@ -3758,30 +4079,10 @@ const loadSelectedSessionMessages = async (sessions, selectedSessionId) => {
|
|
|
3758
4079
|
const loadContent = async (state, savedState) => {
|
|
3759
4080
|
const savedSelectedModelId = getSavedSelectedModelId(savedState);
|
|
3760
4081
|
const savedViewMode = getSavedViewMode(savedState);
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
openApiApiKey = savedOpenApiKey;
|
|
3766
|
-
} else {
|
|
3767
|
-
const legacySavedOpenApiApiKey = await get('secrets.openApiApiKey');
|
|
3768
|
-
if (typeof legacySavedOpenApiApiKey === 'string' && legacySavedOpenApiApiKey) {
|
|
3769
|
-
openApiApiKey = legacySavedOpenApiApiKey;
|
|
3770
|
-
} else {
|
|
3771
|
-
const legacySavedOpenAiApiKey = await get('secrets.openAiApiKey');
|
|
3772
|
-
openApiApiKey = typeof legacySavedOpenAiApiKey === 'string' ? legacySavedOpenAiApiKey : '';
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
} catch {
|
|
3776
|
-
openApiApiKey = '';
|
|
3777
|
-
}
|
|
3778
|
-
let openRouterApiKey = '';
|
|
3779
|
-
try {
|
|
3780
|
-
const savedOpenRouterApiKey = await get('secrets.openRouterApiKey');
|
|
3781
|
-
openRouterApiKey = typeof savedOpenRouterApiKey === 'string' ? savedOpenRouterApiKey : '';
|
|
3782
|
-
} catch {
|
|
3783
|
-
openRouterApiKey = '';
|
|
3784
|
-
}
|
|
4082
|
+
const {
|
|
4083
|
+
openApiApiKey,
|
|
4084
|
+
openRouterApiKey
|
|
4085
|
+
} = await loadPreferences();
|
|
3785
4086
|
const legacySavedSessions = getSavedSessions(savedState);
|
|
3786
4087
|
const storedSessions = await listChatSessions();
|
|
3787
4088
|
let sessions = storedSessions;
|
|
@@ -3799,6 +4100,8 @@ const loadContent = async (state, savedState) => {
|
|
|
3799
4100
|
}
|
|
3800
4101
|
const preferredSessionId = getSavedSelectedSessionId(savedState) || state.selectedSessionId;
|
|
3801
4102
|
const preferredModelId = savedSelectedModelId || state.selectedModelId;
|
|
4103
|
+
const chatListScrollTop = getSavedChatListScrollTop(savedState) ?? state.chatListScrollTop;
|
|
4104
|
+
const messagesScrollTop = getSavedMessagesScrollTop(savedState) ?? state.messagesScrollTop;
|
|
3802
4105
|
const selectedModelId = state.models.some(model => model.id === preferredModelId) ? preferredModelId : state.models[0]?.id || '';
|
|
3803
4106
|
const selectedSessionId = sessions.some(session => session.id === preferredSessionId) ? preferredSessionId : sessions[0]?.id || '';
|
|
3804
4107
|
sessions = await loadSelectedSessionMessages(sessions, selectedSessionId);
|
|
@@ -3806,7 +4109,9 @@ const loadContent = async (state, savedState) => {
|
|
|
3806
4109
|
const viewMode = sessions.length === 0 || !selectedSessionId ? 'list' : preferredViewMode === 'detail' ? 'detail' : 'list';
|
|
3807
4110
|
return {
|
|
3808
4111
|
...state,
|
|
4112
|
+
chatListScrollTop,
|
|
3809
4113
|
initial: false,
|
|
4114
|
+
messagesScrollTop,
|
|
3810
4115
|
openApiApiKey,
|
|
3811
4116
|
openApiApiKeyInput: openApiApiKey,
|
|
3812
4117
|
openRouterApiKey,
|
|
@@ -3852,12 +4157,22 @@ const openMockSession = async (state, mockSessionId, mockChatMessages) => {
|
|
|
3852
4157
|
};
|
|
3853
4158
|
};
|
|
3854
4159
|
|
|
4160
|
+
const getCss = composerHeight => {
|
|
4161
|
+
return `:root {
|
|
4162
|
+
--ChatInputBoxHeight: ${composerHeight}px;
|
|
4163
|
+
}`;
|
|
4164
|
+
};
|
|
4165
|
+
|
|
3855
4166
|
// TODO render things like scrollbar height,scrollbar offset, textarea height,
|
|
3856
4167
|
// list height
|
|
3857
|
-
|
|
3858
|
-
`;
|
|
4168
|
+
|
|
3859
4169
|
const renderCss = (oldState, newState) => {
|
|
3860
|
-
|
|
4170
|
+
const {
|
|
4171
|
+
composerHeight,
|
|
4172
|
+
uid
|
|
4173
|
+
} = newState;
|
|
4174
|
+
const css = getCss(composerHeight);
|
|
4175
|
+
return [SetCss, uid, css];
|
|
3861
4176
|
};
|
|
3862
4177
|
|
|
3863
4178
|
const getFocusSelector = focus => {
|
|
@@ -3933,6 +4248,8 @@ const HandleClickList = 17;
|
|
|
3933
4248
|
const HandleClickDelete = 18;
|
|
3934
4249
|
const HandleSubmit = 19;
|
|
3935
4250
|
const HandleModelChange = 20;
|
|
4251
|
+
const HandleChatListScroll = 21;
|
|
4252
|
+
const HandleMessagesScroll = 22;
|
|
3936
4253
|
|
|
3937
4254
|
const getModelLabel = model => {
|
|
3938
4255
|
if (model.provider === 'openRouter') {
|
|
@@ -4029,7 +4346,7 @@ const getChatSendAreaDom = (composerValue, models, selectedModelId, usageOvervie
|
|
|
4029
4346
|
onFocus: HandleFocus,
|
|
4030
4347
|
onInput: HandleInput,
|
|
4031
4348
|
placeholder: composePlaceholder(),
|
|
4032
|
-
style: `height:${composerHeight}px;font-size:${composerFontSize}px;font-family:${composerFontFamily};line-height:${composerLineHeight}px;`,
|
|
4349
|
+
// style: `height:${composerHeight}px;font-size:${composerFontSize}px;font-family:${composerFontFamily};line-height:${composerLineHeight}px;`,
|
|
4033
4350
|
type: TextArea,
|
|
4034
4351
|
value: composerValue
|
|
4035
4352
|
}, {
|
|
@@ -4240,18 +4557,20 @@ const getEmptyMessagesDom = () => {
|
|
|
4240
4557
|
}, text(startConversation())];
|
|
4241
4558
|
};
|
|
4242
4559
|
|
|
4243
|
-
const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle') => {
|
|
4560
|
+
const getMessagesDom = (messages, openRouterApiKeyInput, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', messagesScrollTop = 0) => {
|
|
4244
4561
|
if (messages.length === 0) {
|
|
4245
4562
|
return getEmptyMessagesDom();
|
|
4246
4563
|
}
|
|
4247
4564
|
return [{
|
|
4248
4565
|
childCount: messages.length,
|
|
4249
4566
|
className: 'ChatMessages',
|
|
4567
|
+
onScroll: HandleMessagesScroll,
|
|
4568
|
+
scrollTop: messagesScrollTop,
|
|
4250
4569
|
type: Div
|
|
4251
4570
|
}, ...messages.flatMap(message => getChatMessageDom(message, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState))];
|
|
4252
4571
|
};
|
|
4253
4572
|
|
|
4254
|
-
const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
|
|
4573
|
+
const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, messagesScrollTop = 0) => {
|
|
4255
4574
|
const selectedSession = sessions.find(session => session.id === selectedSessionId);
|
|
4256
4575
|
const selectedSessionTitle = selectedSession?.title || chatTitle();
|
|
4257
4576
|
const messages = selectedSession ? selectedSession.messages : [];
|
|
@@ -4259,7 +4578,7 @@ const getChatModeDetailVirtualDom = (sessions, selectedSessionId, composerValue,
|
|
|
4259
4578
|
childCount: 3,
|
|
4260
4579
|
className: mergeClassNames(Viewlet, Chat),
|
|
4261
4580
|
type: Div
|
|
4262
|
-
}, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
|
|
4581
|
+
}, ...getChatHeaderDomDetailMode(selectedSessionTitle), ...getMessagesDom(messages, openRouterApiKeyInput, openApiApiKeyInput, openRouterApiKeyState, messagesScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
|
|
4263
4582
|
};
|
|
4264
4583
|
|
|
4265
4584
|
const getChatHeaderListModeDom = () => {
|
|
@@ -4316,7 +4635,7 @@ const getSessionDom = session => {
|
|
|
4316
4635
|
}, text('🗑')];
|
|
4317
4636
|
};
|
|
4318
4637
|
|
|
4319
|
-
const getChatListDom = (sessions, selectedSessionId) => {
|
|
4638
|
+
const getChatListDom = (sessions, selectedSessionId, chatListScrollTop = 0) => {
|
|
4320
4639
|
if (sessions.length === 0) {
|
|
4321
4640
|
return getEmptyChatSessionsDom();
|
|
4322
4641
|
}
|
|
@@ -4324,16 +4643,18 @@ const getChatListDom = (sessions, selectedSessionId) => {
|
|
|
4324
4643
|
childCount: sessions.length,
|
|
4325
4644
|
className: ChatList,
|
|
4326
4645
|
onClick: HandleClickList,
|
|
4646
|
+
onScroll: HandleChatListScroll,
|
|
4647
|
+
scrollTop: chatListScrollTop,
|
|
4327
4648
|
type: Div
|
|
4328
4649
|
}, ...sessions.flatMap(getSessionDom)];
|
|
4329
4650
|
};
|
|
4330
4651
|
|
|
4331
|
-
const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
|
|
4652
|
+
const getChatModeListVirtualDom = (sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0) => {
|
|
4332
4653
|
return [{
|
|
4333
4654
|
childCount: 3,
|
|
4334
4655
|
className: mergeClassNames(Viewlet, Chat),
|
|
4335
4656
|
type: Div
|
|
4336
|
-
}, ...getChatHeaderListModeDom(), ...getChatListDom(sessions), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
|
|
4657
|
+
}, ...getChatHeaderListModeDom(), ...getChatListDom(sessions, selectedSessionId, chatListScrollTop), ...getChatSendAreaDom(composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight)];
|
|
4337
4658
|
};
|
|
4338
4659
|
|
|
4339
4660
|
const getChatModeUnsupportedVirtualDom = () => {
|
|
@@ -4343,12 +4664,12 @@ const getChatModeUnsupportedVirtualDom = () => {
|
|
|
4343
4664
|
}, text(unknownViewMode())];
|
|
4344
4665
|
};
|
|
4345
4666
|
|
|
4346
|
-
const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20) => {
|
|
4667
|
+
const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput = '', openRouterApiKeyState = 'idle', composerHeight = 28, composerFontSize = 13, composerFontFamily = 'system-ui', composerLineHeight = 20, chatListScrollTop = 0, messagesScrollTop = 0) => {
|
|
4347
4668
|
switch (viewMode) {
|
|
4348
4669
|
case 'detail':
|
|
4349
|
-
return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
|
|
4670
|
+
return getChatModeDetailVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, openApiApiKeyInput, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, messagesScrollTop);
|
|
4350
4671
|
case 'list':
|
|
4351
|
-
return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
|
|
4672
|
+
return getChatModeListVirtualDom(sessions, selectedSessionId, composerValue, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop);
|
|
4352
4673
|
default:
|
|
4353
4674
|
return getChatModeUnsupportedVirtualDom();
|
|
4354
4675
|
}
|
|
@@ -4356,12 +4677,14 @@ const getChatVirtualDom = (sessions, selectedSessionId, composerValue, openRoute
|
|
|
4356
4677
|
|
|
4357
4678
|
const renderItems = (oldState, newState) => {
|
|
4358
4679
|
const {
|
|
4680
|
+
chatListScrollTop,
|
|
4359
4681
|
composerFontFamily,
|
|
4360
4682
|
composerFontSize,
|
|
4361
4683
|
composerHeight,
|
|
4362
4684
|
composerLineHeight,
|
|
4363
4685
|
composerValue,
|
|
4364
4686
|
initial,
|
|
4687
|
+
messagesScrollTop,
|
|
4365
4688
|
models,
|
|
4366
4689
|
openApiApiKeyInput,
|
|
4367
4690
|
openRouterApiKeyInput,
|
|
@@ -4378,7 +4701,7 @@ const renderItems = (oldState, newState) => {
|
|
|
4378
4701
|
if (initial) {
|
|
4379
4702
|
return [SetDom2, uid, []];
|
|
4380
4703
|
}
|
|
4381
|
-
const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight);
|
|
4704
|
+
const dom = getChatVirtualDom(sessions, selectedSessionId, composerValue, openRouterApiKeyInput, viewMode, models, selectedModelId, usageOverviewEnabled, tokensUsed, tokensMax, openApiApiKeyInput, openRouterApiKeyState, composerHeight, composerFontSize, composerFontFamily, composerLineHeight, chatListScrollTop, messagesScrollTop);
|
|
4382
4705
|
return [SetDom2, uid, dom];
|
|
4383
4706
|
};
|
|
4384
4707
|
|
|
@@ -4389,6 +4712,14 @@ const renderIncremental = (oldState, newState) => {
|
|
|
4389
4712
|
return [SetPatches, newState.uid, patches];
|
|
4390
4713
|
};
|
|
4391
4714
|
|
|
4715
|
+
const renderScrollTop = (oldState, newState) => {
|
|
4716
|
+
const {
|
|
4717
|
+
messagesScrollTop,
|
|
4718
|
+
uid
|
|
4719
|
+
} = newState;
|
|
4720
|
+
return [SetProperty, uid, '.ChatMessages', 'scrollTop', messagesScrollTop];
|
|
4721
|
+
};
|
|
4722
|
+
|
|
4392
4723
|
const renderValue = (oldState, newState) => {
|
|
4393
4724
|
const {
|
|
4394
4725
|
composerValue
|
|
@@ -4408,6 +4739,8 @@ const getRenderer = diffType => {
|
|
|
4408
4739
|
return renderIncremental;
|
|
4409
4740
|
case RenderItems:
|
|
4410
4741
|
return renderItems;
|
|
4742
|
+
case RenderScrollTop:
|
|
4743
|
+
return renderScrollTop;
|
|
4411
4744
|
case RenderValue:
|
|
4412
4745
|
return renderValue;
|
|
4413
4746
|
default:
|
|
@@ -4469,6 +4802,12 @@ const renderEventListeners = () => {
|
|
|
4469
4802
|
}, {
|
|
4470
4803
|
name: HandleModelChange,
|
|
4471
4804
|
params: ['handleModelChange', TargetValue]
|
|
4805
|
+
}, {
|
|
4806
|
+
name: HandleChatListScroll,
|
|
4807
|
+
params: ['handleChatListScroll', 'event.target.scrollTop']
|
|
4808
|
+
}, {
|
|
4809
|
+
name: HandleMessagesScroll,
|
|
4810
|
+
params: ['handleMessagesScroll', 'event.target.scrollTop']
|
|
4472
4811
|
}, {
|
|
4473
4812
|
name: HandleFocus,
|
|
4474
4813
|
params: ['handleInputFocus', TargetName]
|
|
@@ -4507,8 +4846,10 @@ const resize = (state, dimensions) => {
|
|
|
4507
4846
|
|
|
4508
4847
|
const saveState = state => {
|
|
4509
4848
|
const {
|
|
4849
|
+
chatListScrollTop,
|
|
4510
4850
|
composerValue,
|
|
4511
4851
|
height,
|
|
4852
|
+
messagesScrollTop,
|
|
4512
4853
|
nextMessageId,
|
|
4513
4854
|
renamingSessionId,
|
|
4514
4855
|
selectedModelId,
|
|
@@ -4519,8 +4860,10 @@ const saveState = state => {
|
|
|
4519
4860
|
y
|
|
4520
4861
|
} = state;
|
|
4521
4862
|
return {
|
|
4863
|
+
chatListScrollTop,
|
|
4522
4864
|
composerValue,
|
|
4523
4865
|
height,
|
|
4866
|
+
messagesScrollTop,
|
|
4524
4867
|
nextMessageId,
|
|
4525
4868
|
renamingSessionId,
|
|
4526
4869
|
selectedModelId,
|
|
@@ -4578,6 +4921,7 @@ const commandMap = {
|
|
|
4578
4921
|
'Chat.getKeyBindings': getKeyBindings,
|
|
4579
4922
|
'Chat.getSelectedSessionId': wrapGetter(getSelectedSessionId),
|
|
4580
4923
|
'Chat.handleChatListContextMenu': handleChatListContextMenu,
|
|
4924
|
+
'Chat.handleChatListScroll': wrapCommand(handleChatListScroll),
|
|
4581
4925
|
'Chat.handleClick': wrapCommand(handleClick),
|
|
4582
4926
|
'Chat.handleClickBack': wrapCommand(handleClickBack),
|
|
4583
4927
|
'Chat.handleClickClose': handleClickClose,
|
|
@@ -4588,6 +4932,7 @@ const commandMap = {
|
|
|
4588
4932
|
'Chat.handleInput': wrapCommand(handleInput),
|
|
4589
4933
|
'Chat.handleInputFocus': wrapCommand(handleInputFocus),
|
|
4590
4934
|
'Chat.handleKeyDown': wrapCommand(handleKeyDown),
|
|
4935
|
+
'Chat.handleMessagesScroll': wrapCommand(handleMessagesScroll),
|
|
4591
4936
|
'Chat.handleModelChange': wrapCommand(handleModelChange),
|
|
4592
4937
|
'Chat.handleSubmit': wrapCommand(handleSubmit),
|
|
4593
4938
|
'Chat.initialize': initialize,
|