claude-simple 0.2.3 → 0.2.4
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/client.js +7 -1
- package/dist/{app.0.2.3.min.js → app.0.2.4.min.js} +165 -152
- package/dist/index.html +2 -2
- package/dist/{style.0.2.3.min.css → style.0.2.4.min.css} +61 -0
- package/package.json +1 -1
- package/public/chat.js +201 -50
- package/public/shell/templates.js +7 -4
- package/public/style.css +61 -0
- package/public/sw.js +1 -1
package/dist/index.html
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
6
6
|
<title>Claude Code</title>
|
|
7
|
-
<link rel="stylesheet" href="/style.0.2.
|
|
7
|
+
<link rel="stylesheet" href="/style.0.2.4.min.css">
|
|
8
8
|
<link rel="stylesheet" href="/highlight.min.css">
|
|
9
9
|
<script src="/marked.min.js"></script>
|
|
10
10
|
<script src="/highlight.min.js"></script>
|
|
11
11
|
</head>
|
|
12
12
|
<body class="h-dvh overflow-hidden flex flex-col bg-base-100 text-base-content">
|
|
13
13
|
<div id="root" class="flex flex-col flex-1 overflow-hidden"></div>
|
|
14
|
-
<script type="module" src="/app.0.2.
|
|
14
|
+
<script type="module" src="/app.0.2.4.min.js"></script>
|
|
15
15
|
</body>
|
|
16
16
|
</html>
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
line-height: 1.35 !important;
|
|
31
31
|
}
|
|
32
32
|
#btn-opts,
|
|
33
|
+
#btn-attach,
|
|
33
34
|
#send-btn,
|
|
34
35
|
#stop-btn {
|
|
35
36
|
height: 36px !important;
|
|
@@ -886,11 +887,71 @@ html.pwa-standalone .ma-panel {
|
|
|
886
887
|
/* ── Chat composer ───────────────────────────────────────────────────────── */
|
|
887
888
|
/* Single outer border only — no DaisyUI nested focus ring / inner outline. */
|
|
888
889
|
#composer-row #btn-opts,
|
|
890
|
+
#composer-row #btn-attach,
|
|
889
891
|
#composer-row #send-btn,
|
|
890
892
|
#composer-row #stop-btn {
|
|
891
893
|
height: 40px;
|
|
892
894
|
min-height: 40px;
|
|
893
895
|
}
|
|
896
|
+
#chat-composer.chat-drop-active {
|
|
897
|
+
outline: 2px dashed color-mix(in oklch, var(--color-primary) 70%, transparent);
|
|
898
|
+
outline-offset: -2px;
|
|
899
|
+
background: color-mix(in oklch, var(--color-primary) 8%, var(--color-base-200));
|
|
900
|
+
}
|
|
901
|
+
#chat-attach-bar:not(.hidden) {
|
|
902
|
+
display: flex;
|
|
903
|
+
}
|
|
904
|
+
.chat-thumb {
|
|
905
|
+
width: 56px;
|
|
906
|
+
height: 56px;
|
|
907
|
+
border-radius: 8px;
|
|
908
|
+
overflow: hidden;
|
|
909
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 14%, transparent);
|
|
910
|
+
position: relative;
|
|
911
|
+
flex-shrink: 0;
|
|
912
|
+
background: var(--color-base-100);
|
|
913
|
+
}
|
|
914
|
+
.chat-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
|
915
|
+
.chat-thumb-path {
|
|
916
|
+
display: flex;
|
|
917
|
+
align-items: center;
|
|
918
|
+
justify-content: center;
|
|
919
|
+
width: 100%;
|
|
920
|
+
height: 100%;
|
|
921
|
+
font-size: 10px;
|
|
922
|
+
padding: 4px;
|
|
923
|
+
text-align: center;
|
|
924
|
+
word-break: break-all;
|
|
925
|
+
line-height: 1.15;
|
|
926
|
+
color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
|
|
927
|
+
background: color-mix(in oklch, var(--color-base-content) 6%, var(--color-base-100));
|
|
928
|
+
}
|
|
929
|
+
.chat-thumb-label {
|
|
930
|
+
position: absolute;
|
|
931
|
+
left: 0;
|
|
932
|
+
bottom: 0;
|
|
933
|
+
right: 0;
|
|
934
|
+
font-size: 9px;
|
|
935
|
+
text-align: center;
|
|
936
|
+
background: rgba(0,0,0,.55);
|
|
937
|
+
color: #fff;
|
|
938
|
+
line-height: 1.2;
|
|
939
|
+
padding: 1px 0;
|
|
940
|
+
}
|
|
941
|
+
.chat-thumb-x {
|
|
942
|
+
position: absolute;
|
|
943
|
+
top: 0;
|
|
944
|
+
right: 0;
|
|
945
|
+
background: rgba(0,0,0,.65);
|
|
946
|
+
color: #fff;
|
|
947
|
+
border: 0;
|
|
948
|
+
font-size: 10px;
|
|
949
|
+
width: 16px;
|
|
950
|
+
height: 16px;
|
|
951
|
+
cursor: pointer;
|
|
952
|
+
line-height: 1;
|
|
953
|
+
z-index: 1;
|
|
954
|
+
}
|
|
894
955
|
#chat-input.chat-input,
|
|
895
956
|
#chat-input {
|
|
896
957
|
width: 100%;
|
package/package.json
CHANGED
package/public/chat.js
CHANGED
|
@@ -3,7 +3,7 @@ import { watch, delegate, esc, $ } from './lib.js';
|
|
|
3
3
|
import { ctx, isProcessing, currentProject, currentTab, currentModel, currentEffort, currentPermission,
|
|
4
4
|
currentAgent, setAgent, AGENT_LABELS, getDefaultModel, getModelsForAgent, getEffortsForModel,
|
|
5
5
|
addCustomModel, chatDensity, wsStatus } from './state.js';
|
|
6
|
-
import { sendWs, api, flushWsQueue, clearWsQueue } from './api.js';
|
|
6
|
+
import { sendWs, api, authHeaders, flushWsQueue, clearWsQueue } from './api.js';
|
|
7
7
|
import { initWakeLock, setWakeLockDesired } from './wake-lock.js';
|
|
8
8
|
|
|
9
9
|
function assistantLabel() {
|
|
@@ -706,6 +706,7 @@ export function sendMessage() {
|
|
|
706
706
|
input.value = '';
|
|
707
707
|
autoResize(input);
|
|
708
708
|
updateInputMode('');
|
|
709
|
+
clearPendingAttachments();
|
|
709
710
|
appendSystemMsg(`已排队下一条,当前回复结束后发送:${text.length > 80 ? text.slice(0, 80) + '…' : text}`);
|
|
710
711
|
return;
|
|
711
712
|
}
|
|
@@ -713,6 +714,7 @@ export function sendMessage() {
|
|
|
713
714
|
input.value = '';
|
|
714
715
|
autoResize(input);
|
|
715
716
|
updateInputMode('');
|
|
717
|
+
clearPendingAttachments();
|
|
716
718
|
|
|
717
719
|
if (text.startsWith('/')) {
|
|
718
720
|
if (handleSlashCommand(text)) {
|
|
@@ -899,74 +901,195 @@ export function appendMsg(role, _label, text, opts = {}) {
|
|
|
899
901
|
|
|
900
902
|
export function appendSystemMsg(text) { appendMsg('system', '', text); }
|
|
901
903
|
|
|
902
|
-
|
|
904
|
+
/** Pending chat attachments shown above the input (paths also inserted into text). */
|
|
905
|
+
const pendingAttachments = [];
|
|
906
|
+
let attachSeq = 0;
|
|
907
|
+
let chatDropBound = false;
|
|
908
|
+
|
|
909
|
+
async function uploadChatBlob(blob, filename) {
|
|
910
|
+
const name = filename || `upload-${Date.now()}`;
|
|
911
|
+
const headers = authHeaders({ 'x-filename': name });
|
|
912
|
+
delete headers['Content-Type'];
|
|
913
|
+
// Prefer file MIME so hub base64-encodes binary uploads correctly.
|
|
914
|
+
if (blob?.type && !headers['Content-Type'] && !headers['content-type']) {
|
|
915
|
+
headers['Content-Type'] = blob.type;
|
|
916
|
+
}
|
|
903
917
|
const res = await fetch('/api/upload-image', {
|
|
904
918
|
method: 'POST',
|
|
905
|
-
headers
|
|
919
|
+
headers,
|
|
906
920
|
body: blob,
|
|
907
921
|
});
|
|
908
|
-
if (!res.ok)
|
|
909
|
-
|
|
922
|
+
if (!res.ok) {
|
|
923
|
+
const err = await res.json().catch(() => ({}));
|
|
924
|
+
throw new Error(err.error || `Upload failed: ${res.status}`);
|
|
925
|
+
}
|
|
926
|
+
const data = await res.json();
|
|
927
|
+
if (!data?.path) throw new Error('upload returned no path');
|
|
928
|
+
return data.path;
|
|
910
929
|
}
|
|
911
930
|
|
|
912
931
|
function insertAtCursor(ta, text) {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
932
|
+
if (!ta) return;
|
|
933
|
+
const s = ta.selectionStart ?? ta.value.length;
|
|
934
|
+
const e = ta.selectionEnd ?? ta.value.length;
|
|
935
|
+
const padBefore = s > 0 && ta.value[s - 1] !== '\n' ? '\n' : '';
|
|
936
|
+
const insert = padBefore + text + '\n';
|
|
937
|
+
ta.value = ta.value.slice(0, s) + insert + ta.value.slice(e);
|
|
938
|
+
ta.selectionStart = ta.selectionEnd = s + insert.length;
|
|
916
939
|
ta.dispatchEvent(new Event('input'));
|
|
917
940
|
}
|
|
918
941
|
|
|
942
|
+
function isImageFile(file) {
|
|
943
|
+
if (!file) return false;
|
|
944
|
+
if (file.type && file.type.startsWith('image/')) return true;
|
|
945
|
+
return /\.(png|jpe?g|gif|webp|bmp|svg|heic|heif|avif)$/i.test(file.name || '');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function makeAttachRef(file, path) {
|
|
949
|
+
const name = file?.name || path.split('/').pop() || 'file';
|
|
950
|
+
if (isImageFile(file)) {
|
|
951
|
+
attachSeq += 1;
|
|
952
|
+
const label = `img${attachSeq}`;
|
|
953
|
+
return { ref: ``, label, kind: 'image' };
|
|
954
|
+
}
|
|
955
|
+
attachSeq += 1;
|
|
956
|
+
const label = `file${attachSeq}`;
|
|
957
|
+
return { ref: `[${label}](${path})`, label, kind: 'file' };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function clearPendingAttachments() {
|
|
961
|
+
for (const item of pendingAttachments) {
|
|
962
|
+
if (item.previewUrl) {
|
|
963
|
+
try { URL.revokeObjectURL(item.previewUrl); } catch { /* ignore */ }
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
pendingAttachments.length = 0;
|
|
967
|
+
renderAttachBar();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function renderAttachBar() {
|
|
971
|
+
const bar = $('chat-attach-bar');
|
|
972
|
+
if (!bar) return;
|
|
973
|
+
if (!pendingAttachments.length) {
|
|
974
|
+
bar.classList.add('hidden');
|
|
975
|
+
bar.innerHTML = '';
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
bar.classList.remove('hidden');
|
|
979
|
+
bar.innerHTML = pendingAttachments.map((item, i) => `
|
|
980
|
+
<div class="chat-thumb relative" title="${esc(item.ref || item.path)}">
|
|
981
|
+
${item.previewUrl
|
|
982
|
+
? `<img src="${item.previewUrl}" alt="${esc(item.label || 'img')}">`
|
|
983
|
+
: `<span class="chat-thumb-path">${esc(item.label || item.name || 'file')}</span>`}
|
|
984
|
+
<span class="chat-thumb-label">${esc(item.label || item.kind || '')}</span>
|
|
985
|
+
<button type="button" class="chat-thumb-x" data-rm-attach="${i}" aria-label="remove">✕</button>
|
|
986
|
+
</div>`).join('');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async function addChatAttachment(file) {
|
|
990
|
+
if (!file) return;
|
|
991
|
+
const name = file.name || `upload-${Date.now()}`;
|
|
992
|
+
const path = await uploadChatBlob(file, name);
|
|
993
|
+
const { ref, label, kind } = makeAttachRef(file, path);
|
|
994
|
+
let previewUrl = '';
|
|
995
|
+
if (kind === 'image') {
|
|
996
|
+
try { previewUrl = URL.createObjectURL(file); } catch { /* ignore */ }
|
|
997
|
+
}
|
|
998
|
+
pendingAttachments.push({ path, ref, label, kind, name, previewUrl });
|
|
999
|
+
renderAttachBar();
|
|
1000
|
+
insertAtCursor($('chat-input'), ref);
|
|
1001
|
+
return path;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async function uploadFilesList(fileList) {
|
|
1005
|
+
const files = [...(fileList || [])].filter(Boolean);
|
|
1006
|
+
if (!files.length) return;
|
|
1007
|
+
const ta = $('chat-input');
|
|
1008
|
+
if (ta) ta.disabled = true;
|
|
1009
|
+
try {
|
|
1010
|
+
for (const file of files) {
|
|
1011
|
+
try {
|
|
1012
|
+
await addChatAttachment(file);
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
appendSystemMsg(`上传失败 ${file.name || 'file'}: ${err.message}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
} finally {
|
|
1018
|
+
if (ta) {
|
|
1019
|
+
ta.disabled = false;
|
|
1020
|
+
ta.focus();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** Open file picker (images + any files). */
|
|
1026
|
+
export function attachImage() {
|
|
1027
|
+
const input = $('chat-file-input');
|
|
1028
|
+
if (input) {
|
|
1029
|
+
input.value = '';
|
|
1030
|
+
input.click();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const fallback = document.createElement('input');
|
|
1034
|
+
fallback.type = 'file';
|
|
1035
|
+
fallback.accept = 'image/*,*/*';
|
|
1036
|
+
fallback.multiple = true;
|
|
1037
|
+
fallback.addEventListener('change', () => uploadFilesList(fallback.files));
|
|
1038
|
+
fallback.click();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
919
1041
|
export function initImagePaste() {
|
|
920
1042
|
document.addEventListener('paste', async e => {
|
|
921
1043
|
const ta = $('chat-input');
|
|
922
|
-
if (!ta
|
|
1044
|
+
if (!ta) return;
|
|
1045
|
+
// Accept paste when focusing input, or when composer is active
|
|
1046
|
+
const composer = $('chat-composer');
|
|
1047
|
+
const inComposer = composer && (composer.contains(document.activeElement) || document.activeElement === ta);
|
|
1048
|
+
if (!inComposer && document.activeElement !== ta) return;
|
|
1049
|
+
|
|
923
1050
|
const items = [...(e.clipboardData?.items || [])];
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
ta.disabled = true;
|
|
931
|
-
try {
|
|
932
|
-
const path = await uploadImageBlob(blob, name);
|
|
933
|
-
insertAtCursor(ta, ``);
|
|
934
|
-
} catch (err) {
|
|
935
|
-
appendSystemMsg(`Image paste failed: ${err.message}`);
|
|
936
|
-
} finally {
|
|
937
|
-
ta.disabled = false;
|
|
938
|
-
ta.focus();
|
|
1051
|
+
const files = [];
|
|
1052
|
+
for (const it of items) {
|
|
1053
|
+
if (it.kind === 'file') {
|
|
1054
|
+
const f = it.getAsFile();
|
|
1055
|
+
if (f) files.push(f);
|
|
1056
|
+
}
|
|
939
1057
|
}
|
|
1058
|
+
// Prefer image paste; also allow non-image files from clipboard when present
|
|
1059
|
+
const imageFiles = files.filter(f => isImageFile(f));
|
|
1060
|
+
const pick = imageFiles.length ? imageFiles : files;
|
|
1061
|
+
if (!pick.length) return;
|
|
1062
|
+
e.preventDefault();
|
|
1063
|
+
await uploadFilesList(pick);
|
|
940
1064
|
});
|
|
941
|
-
}
|
|
942
1065
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
// Insert path reference into chat input
|
|
959
|
-
const ta = $('chat-input');
|
|
960
|
-
if (ta) {
|
|
961
|
-
ta.value += (ta.value ? '\n' : '') + path;
|
|
962
|
-
ta.dispatchEvent(new Event('input'));
|
|
963
|
-
ta.focus();
|
|
964
|
-
}
|
|
965
|
-
appendSystemMsg(`📎 ${path}`);
|
|
966
|
-
} catch (e) { appendSystemMsg(`Upload failed: ${e.message}`); }
|
|
1066
|
+
if (chatDropBound) return;
|
|
1067
|
+
chatDropBound = true;
|
|
1068
|
+
document.addEventListener('dragover', e => {
|
|
1069
|
+
const composer = $('chat-composer');
|
|
1070
|
+
if (!composer || !e.dataTransfer?.types?.includes('Files')) return;
|
|
1071
|
+
if (!composer.contains(e.target) && e.target !== composer) return;
|
|
1072
|
+
e.preventDefault();
|
|
1073
|
+
composer.classList.add('chat-drop-active');
|
|
1074
|
+
});
|
|
1075
|
+
document.addEventListener('dragleave', e => {
|
|
1076
|
+
const composer = $('chat-composer');
|
|
1077
|
+
if (!composer) return;
|
|
1078
|
+
if (e.target === composer || composer.contains(e.target)) {
|
|
1079
|
+
// leaving to outside
|
|
1080
|
+
if (!composer.contains(e.relatedTarget)) composer.classList.remove('chat-drop-active');
|
|
967
1081
|
}
|
|
968
1082
|
});
|
|
969
|
-
|
|
1083
|
+
document.addEventListener('drop', async e => {
|
|
1084
|
+
const composer = $('chat-composer');
|
|
1085
|
+
if (!composer) return;
|
|
1086
|
+
composer.classList.remove('chat-drop-active');
|
|
1087
|
+
if (!composer.contains(e.target) && e.target !== composer) return;
|
|
1088
|
+
const files = [...(e.dataTransfer?.files || [])];
|
|
1089
|
+
if (!files.length) return;
|
|
1090
|
+
e.preventDefault();
|
|
1091
|
+
await uploadFilesList(files);
|
|
1092
|
+
});
|
|
970
1093
|
}
|
|
971
1094
|
|
|
972
1095
|
function fmtNum(n) { return n >= 1000 ? `${(n/1000).toFixed(1)}k` : String(n); }
|
|
@@ -1315,6 +1438,34 @@ export function initChat() {
|
|
|
1315
1438
|
});
|
|
1316
1439
|
delegate.on('input', '#chat-input', (e, el) => { autoResize(el); updateInputMode(el.value); });
|
|
1317
1440
|
|
|
1441
|
+
// Attach images / files
|
|
1442
|
+
delegate.on('click', '#btn-attach', e => {
|
|
1443
|
+
e.preventDefault();
|
|
1444
|
+
attachImage();
|
|
1445
|
+
});
|
|
1446
|
+
delegate.on('change', '#chat-file-input', (e, el) => {
|
|
1447
|
+
const files = el?.files;
|
|
1448
|
+
if (files?.length) uploadFilesList(files);
|
|
1449
|
+
if (el) el.value = '';
|
|
1450
|
+
});
|
|
1451
|
+
delegate.on('click', '[data-rm-attach]', (e, el) => {
|
|
1452
|
+
e.preventDefault();
|
|
1453
|
+
e.stopPropagation();
|
|
1454
|
+
const i = parseInt(el.dataset.rmAttach, 10);
|
|
1455
|
+
if (Number.isNaN(i) || i < 0 || i >= pendingAttachments.length) return;
|
|
1456
|
+
const item = pendingAttachments[i];
|
|
1457
|
+
if (item?.previewUrl) {
|
|
1458
|
+
try { URL.revokeObjectURL(item.previewUrl); } catch { /* ignore */ }
|
|
1459
|
+
}
|
|
1460
|
+
const ta = $('chat-input');
|
|
1461
|
+
if (ta && item?.ref) {
|
|
1462
|
+
ta.value = ta.value.split(item.ref).join('').replace(/\n{3,}/g, '\n\n');
|
|
1463
|
+
ta.dispatchEvent(new Event('input'));
|
|
1464
|
+
}
|
|
1465
|
+
pendingAttachments.splice(i, 1);
|
|
1466
|
+
renderAttachBar();
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1318
1469
|
// Permission buttons → approval.respond (typed control plane)
|
|
1319
1470
|
delegate.on('click', '[data-perm-id]', (e, el) => {
|
|
1320
1471
|
sendWs({
|
|
@@ -239,6 +239,7 @@ export const AppShell = (opts = {}) => {
|
|
|
239
239
|
</div>
|
|
240
240
|
<div id="permission-requests"></div>
|
|
241
241
|
<div id="chat-composer" class="flex flex-col border-t border-base-300 bg-base-200 flex-shrink-0">
|
|
242
|
+
<div id="chat-attach-bar" class="hidden flex flex-wrap gap-1.5 px-3 pt-2"></div>
|
|
242
243
|
<div id="chat-opts" class="hidden flex items-center gap-1.5 px-3 pt-2 flex-wrap">
|
|
243
244
|
<select id="sel-agent" class="select select-xs select-bordered text-xs font-semibold" title="Agent">
|
|
244
245
|
<option value="claude">Claude</option>
|
|
@@ -283,8 +284,10 @@ export const AppShell = (opts = {}) => {
|
|
|
283
284
|
</select>
|
|
284
285
|
</div>
|
|
285
286
|
<div id="composer-row" class="flex gap-2 items-end px-3 pt-2 pb-2">
|
|
286
|
-
<button id="btn-opts" class="btn btn-ghost btn-sm px-2 text-base-content/30 hover:text-base-content flex-shrink-0" title="Options">⚙</button>
|
|
287
|
-
<
|
|
287
|
+
<button type="button" id="btn-opts" class="btn btn-ghost btn-sm px-2 text-base-content/30 hover:text-base-content flex-shrink-0" title="Options">⚙</button>
|
|
288
|
+
<button type="button" id="btn-attach" class="btn btn-ghost btn-sm px-2 text-base-content/40 hover:text-base-content flex-shrink-0" title="添加图片 / 文件">📎</button>
|
|
289
|
+
<input type="file" id="chat-file-input" class="hidden" multiple accept="image/*,*/*">
|
|
290
|
+
<textarea id="chat-input" rows="1" placeholder="Ask… (📎 图片/文件 · !cmd · ⌘↵)"
|
|
288
291
|
class="chat-input flex-1 text-sm resize-none leading-relaxed"
|
|
289
292
|
name="agent-chat-message"
|
|
290
293
|
autocomplete="off"
|
|
@@ -295,8 +298,8 @@ export const AppShell = (opts = {}) => {
|
|
|
295
298
|
data-lpignore="true"
|
|
296
299
|
data-form-type="other"
|
|
297
300
|
data-bwignore="true"></textarea>
|
|
298
|
-
<button id="send-btn" class="btn btn-primary btn-sm flex-shrink-0">Send</button>
|
|
299
|
-
<button id="stop-btn" class="btn btn-error btn-sm hidden flex-shrink-0">■</button>
|
|
301
|
+
<button type="button" id="send-btn" class="btn btn-primary btn-sm flex-shrink-0">Send</button>
|
|
302
|
+
<button type="button" id="stop-btn" class="btn btn-error btn-sm hidden flex-shrink-0">■</button>
|
|
300
303
|
</div>
|
|
301
304
|
</div>
|
|
302
305
|
</div>
|
package/public/style.css
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
line-height: 1.35 !important;
|
|
29
29
|
}
|
|
30
30
|
#btn-opts,
|
|
31
|
+
#btn-attach,
|
|
31
32
|
#send-btn,
|
|
32
33
|
#stop-btn {
|
|
33
34
|
height: 36px !important;
|
|
@@ -884,11 +885,71 @@ html.pwa-standalone .ma-panel {
|
|
|
884
885
|
/* ── Chat composer ───────────────────────────────────────────────────────── */
|
|
885
886
|
/* Single outer border only — no DaisyUI nested focus ring / inner outline. */
|
|
886
887
|
#composer-row #btn-opts,
|
|
888
|
+
#composer-row #btn-attach,
|
|
887
889
|
#composer-row #send-btn,
|
|
888
890
|
#composer-row #stop-btn {
|
|
889
891
|
height: 40px;
|
|
890
892
|
min-height: 40px;
|
|
891
893
|
}
|
|
894
|
+
#chat-composer.chat-drop-active {
|
|
895
|
+
outline: 2px dashed color-mix(in oklch, var(--color-primary) 70%, transparent);
|
|
896
|
+
outline-offset: -2px;
|
|
897
|
+
background: color-mix(in oklch, var(--color-primary) 8%, var(--color-base-200));
|
|
898
|
+
}
|
|
899
|
+
#chat-attach-bar:not(.hidden) {
|
|
900
|
+
display: flex;
|
|
901
|
+
}
|
|
902
|
+
.chat-thumb {
|
|
903
|
+
width: 56px;
|
|
904
|
+
height: 56px;
|
|
905
|
+
border-radius: 8px;
|
|
906
|
+
overflow: hidden;
|
|
907
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 14%, transparent);
|
|
908
|
+
position: relative;
|
|
909
|
+
flex-shrink: 0;
|
|
910
|
+
background: var(--color-base-100);
|
|
911
|
+
}
|
|
912
|
+
.chat-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
|
913
|
+
.chat-thumb-path {
|
|
914
|
+
display: flex;
|
|
915
|
+
align-items: center;
|
|
916
|
+
justify-content: center;
|
|
917
|
+
width: 100%;
|
|
918
|
+
height: 100%;
|
|
919
|
+
font-size: 10px;
|
|
920
|
+
padding: 4px;
|
|
921
|
+
text-align: center;
|
|
922
|
+
word-break: break-all;
|
|
923
|
+
line-height: 1.15;
|
|
924
|
+
color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
|
|
925
|
+
background: color-mix(in oklch, var(--color-base-content) 6%, var(--color-base-100));
|
|
926
|
+
}
|
|
927
|
+
.chat-thumb-label {
|
|
928
|
+
position: absolute;
|
|
929
|
+
left: 0;
|
|
930
|
+
bottom: 0;
|
|
931
|
+
right: 0;
|
|
932
|
+
font-size: 9px;
|
|
933
|
+
text-align: center;
|
|
934
|
+
background: rgba(0,0,0,.55);
|
|
935
|
+
color: #fff;
|
|
936
|
+
line-height: 1.2;
|
|
937
|
+
padding: 1px 0;
|
|
938
|
+
}
|
|
939
|
+
.chat-thumb-x {
|
|
940
|
+
position: absolute;
|
|
941
|
+
top: 0;
|
|
942
|
+
right: 0;
|
|
943
|
+
background: rgba(0,0,0,.65);
|
|
944
|
+
color: #fff;
|
|
945
|
+
border: 0;
|
|
946
|
+
font-size: 10px;
|
|
947
|
+
width: 16px;
|
|
948
|
+
height: 16px;
|
|
949
|
+
cursor: pointer;
|
|
950
|
+
line-height: 1;
|
|
951
|
+
z-index: 1;
|
|
952
|
+
}
|
|
892
953
|
#chat-input.chat-input,
|
|
893
954
|
#chat-input {
|
|
894
955
|
width: 100%;
|
package/public/sw.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Agent UI service worker — cache shell assets only.
|
|
2
2
|
* Never intercepts WebSocket or mutates API semantics beyond offline fallback.
|
|
3
3
|
* Bump CACHE when shipping UI that must invalidate old shells. */
|
|
4
|
-
const CACHE = 'agent-ui-shell-
|
|
4
|
+
const CACHE = 'agent-ui-shell-v8';
|
|
5
5
|
const PRECACHE = [
|
|
6
6
|
'/',
|
|
7
7
|
'/index.html',
|