@liguoshuai/pi-web-chat 1.10.1 → 1.10.3
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/docs/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/public/app.js +204 -36
- package/public/index.html +5 -5
- package/public/style.css +156 -34
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.10.3] - 2026-08-30
|
|
11
|
+
|
|
12
|
+
### Fixed & Improved
|
|
13
|
+
- **移动端附件导入功能全面重构与修复 (Mobile Attachment Import Fix)**:
|
|
14
|
+
- **移动端文件选择器唤起修复**:将上传触发器重构为标准可访问的 `<label for="imageFileInput">` 并配合 CSS `.sr-only-file-input`,彻底解决 iOS Safari、Android Chrome、移动端 WebView 及微信内置浏览器中因 `display: none` 导致异步 JS `.click()` 被浏览器安全策略拦截而无法调起系统相册/文件选择器的问题。
|
|
15
|
+
- **移动端格式与相机照片兼容性**:扩展对 iOS HEIC/HEIF 相机原图、AVIF、ICO、SVG 及大写扩展名的识别与支持。
|
|
16
|
+
- **移动端高清原图智能压缩降采样 (Client-side Downscale)**:针对手机拍摄的超高分辨率照片(12MP~48MP),自动使用 Canvas 进行等比下采样(最大 2048px)与高质量编码压缩,防止超大 Base64 数据挤爆移动端内存或造成 WebSocket 通信中断,同时确保所有主流大模型 Vision API 稳定解析。
|
|
17
|
+
- **文本与源码文件附件导入支持 (Text & Code Attachment Parsing)**:支持通过附件按钮、拖拽或剪贴板直接导入 `.py`、`.js`、`.json`、`.md`、`.txt`、`.log` 等各类代码和文本附件,自动提取并按对应语言 Markdown 代码块注入输入框。
|
|
18
|
+
- **预览交互与触控体验优化 (Touch UX & Preview Lightbox)**:输入框待发送图片缩略图支持点击直接弹出 Lightbox 大图预览,加大移动端单张删除按钮的触控热区(22px)与操作反馈。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [1.10.2] - 2026-08-30
|
|
23
|
+
|
|
24
|
+
### Fixed & Improved
|
|
25
|
+
- **移动端顶栏排版与信息密度重构 (Mobile Topbar UX)**:
|
|
26
|
+
- 在移动端隐藏多余的会话名截断占位,隐藏导出按钮文字标签仅保留操作图标,为工作目录与模型选择胶囊腾出充足显示空间。
|
|
27
|
+
- 优化移动端触控胶囊布局与间距,杜绝文字极端挤压和残缺问题。
|
|
28
|
+
- **欢迎区与模型卡片响应式布局优化 (Empty State & Model Banner)**:
|
|
29
|
+
- 优化欢迎引导文案,兼容移动端抽屉式侧边栏交互。
|
|
30
|
+
- 为 `★ 默认模型` 徽标增加专用样式并强制单行禁止折行,彻底消除断词分行问题。
|
|
31
|
+
- 修复模型卡片在移动端下的左右对齐与视觉比例,“切换模型”按钮平滑对齐。
|
|
32
|
+
- 优化快捷推荐指令标签(Suggestions Chips),支持移动端优雅折行排版。
|
|
33
|
+
- **移动端输入框与占位提示适配 (Composer Mobile Adaptation)**:
|
|
34
|
+
- 移动端自动使用精简的 Placeholder 提示文案,彻底解决长提示在窄屏单行输入框下文字被腰斩截断的问题。
|
|
35
|
+
- 增强底部操作区全面屏手势安全区(`safe-area-inset-bottom`)适配。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
10
39
|
## [1.10.1] - 2026-08-30
|
|
11
40
|
|
|
12
41
|
### Improved
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -812,7 +812,11 @@ function renderImagePreviews() {
|
|
|
812
812
|
}
|
|
813
813
|
bar.style.display = "flex";
|
|
814
814
|
state.attachedImages.forEach((img, idx) => {
|
|
815
|
-
const item = el("div", {
|
|
815
|
+
const item = el("div", {
|
|
816
|
+
class: "image-preview-item",
|
|
817
|
+
title: "点击查看大图",
|
|
818
|
+
onclick: () => openLightbox(img.url)
|
|
819
|
+
}, [
|
|
816
820
|
el("img", { src: img.url, alt: "预览" }),
|
|
817
821
|
el("button", {
|
|
818
822
|
class: "image-preview-remove",
|
|
@@ -840,38 +844,173 @@ function detectImageMimeType(file) {
|
|
|
840
844
|
bmp: "image/bmp",
|
|
841
845
|
svg: "image/svg+xml",
|
|
842
846
|
ico: "image/x-icon",
|
|
843
|
-
avif: "image/avif"
|
|
847
|
+
avif: "image/avif",
|
|
848
|
+
heic: "image/heic",
|
|
849
|
+
heif: "image/heif"
|
|
844
850
|
};
|
|
845
851
|
return map[ext] || "image/png";
|
|
846
852
|
}
|
|
847
853
|
|
|
848
|
-
function
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
return (f.type && f.type.startsWith("image/")) ||
|
|
852
|
-
(f.name && /\.(png|jpe?g|webp|gif|bmp|svg|ico|avif)$/i.test(f.name));
|
|
853
|
-
});
|
|
854
|
-
if (imageFiles.length === 0) return;
|
|
855
|
-
|
|
856
|
-
imageFiles.forEach(file => {
|
|
854
|
+
async function processImageFile(file) {
|
|
855
|
+
const mimeType = detectImageMimeType(file);
|
|
856
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
857
857
|
const reader = new FileReader();
|
|
858
|
-
reader.onload = () =>
|
|
859
|
-
|
|
860
|
-
if (typeof result !== "string") return;
|
|
861
|
-
const commaIdx = result.indexOf(",");
|
|
862
|
-
const base64 = commaIdx !== -1 ? result.slice(commaIdx + 1) : result;
|
|
863
|
-
const mimeType = detectImageMimeType(file);
|
|
864
|
-
state.attachedImages.push({
|
|
865
|
-
data: base64,
|
|
866
|
-
mimeType,
|
|
867
|
-
url: result
|
|
868
|
-
});
|
|
869
|
-
renderImagePreviews();
|
|
870
|
-
};
|
|
858
|
+
reader.onload = () => resolve(reader.result);
|
|
859
|
+
reader.onerror = reject;
|
|
871
860
|
reader.readAsDataURL(file);
|
|
872
861
|
});
|
|
862
|
+
|
|
863
|
+
if (typeof dataUrl !== "string") return null;
|
|
864
|
+
|
|
865
|
+
// For SVG or GIF (which might be animated), or small images, keep original
|
|
866
|
+
if (mimeType.includes("svg") || mimeType.includes("gif") || (file.size && file.size < 800 * 1024)) {
|
|
867
|
+
const commaIdx = dataUrl.indexOf(",");
|
|
868
|
+
const base64 = commaIdx !== -1 ? dataUrl.slice(commaIdx + 1) : dataUrl;
|
|
869
|
+
return { data: base64, mimeType, url: dataUrl };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Optimize large phone camera photos / oversized images via canvas downscale
|
|
873
|
+
try {
|
|
874
|
+
const img = await new Promise((resolve, reject) => {
|
|
875
|
+
const i = new Image();
|
|
876
|
+
i.onload = () => resolve(i);
|
|
877
|
+
i.onerror = reject;
|
|
878
|
+
i.src = dataUrl;
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
const maxDim = 2048;
|
|
882
|
+
let { width, height } = img;
|
|
883
|
+
if (width > maxDim || height > maxDim) {
|
|
884
|
+
if (width > height) {
|
|
885
|
+
height = Math.round((height * maxDim) / width);
|
|
886
|
+
width = maxDim;
|
|
887
|
+
} else {
|
|
888
|
+
width = Math.round((width * maxDim) / height);
|
|
889
|
+
height = maxDim;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const canvas = document.createElement("canvas");
|
|
894
|
+
canvas.width = width;
|
|
895
|
+
canvas.height = height;
|
|
896
|
+
const ctx = canvas.getContext("2d");
|
|
897
|
+
if (!ctx) throw new Error("Canvas 2D unavailable");
|
|
898
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
899
|
+
|
|
900
|
+
const targetMime = (mimeType === "image/png" && file.size < 2 * 1024 * 1024) ? "image/png" : "image/jpeg";
|
|
901
|
+
const quality = 0.88;
|
|
902
|
+
const optimizedUrl = canvas.toDataURL(targetMime, quality);
|
|
903
|
+
const commaIdx = optimizedUrl.indexOf(",");
|
|
904
|
+
const base64 = commaIdx !== -1 ? optimizedUrl.slice(commaIdx + 1) : optimizedUrl;
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
data: base64,
|
|
908
|
+
mimeType: targetMime,
|
|
909
|
+
url: optimizedUrl
|
|
910
|
+
};
|
|
911
|
+
} catch {
|
|
912
|
+
// Fallback to original base64 if canvas processing is unsupported
|
|
913
|
+
const commaIdx = dataUrl.indexOf(",");
|
|
914
|
+
const base64 = commaIdx !== -1 ? dataUrl.slice(commaIdx + 1) : dataUrl;
|
|
915
|
+
return { data: base64, mimeType, url: dataUrl };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function getLanguageFromFilename(filename) {
|
|
920
|
+
if (!filename) return "";
|
|
921
|
+
const ext = filename.split(".").pop().toLowerCase();
|
|
922
|
+
const langMap = {
|
|
923
|
+
js: "javascript", mjs: "javascript", cjs: "javascript",
|
|
924
|
+
ts: "typescript", tsx: "tsx", jsx: "jsx",
|
|
925
|
+
py: "python", pyw: "python",
|
|
926
|
+
rb: "ruby", rs: "rust", go: "go", java: "java",
|
|
927
|
+
c: "c", cpp: "cpp", cc: "cpp", cxx: "cpp", h: "c", hpp: "cpp",
|
|
928
|
+
sh: "bash", bash: "bash", zsh: "bash",
|
|
929
|
+
json: "json", yaml: "yaml", yml: "yaml", toml: "toml",
|
|
930
|
+
md: "markdown", markdown: "markdown",
|
|
931
|
+
html: "html", htm: "html", css: "css", scss: "scss", less: "less",
|
|
932
|
+
sql: "sql", xml: "xml", svg: "xml",
|
|
933
|
+
log: "log", env: "ini", ini: "ini", conf: "ini",
|
|
934
|
+
diff: "diff", patch: "diff", dockerfile: "dockerfile", makefile: "makefile"
|
|
935
|
+
};
|
|
936
|
+
return langMap[ext] || "";
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function isTextFile(file) {
|
|
940
|
+
if (file.type && (file.type.startsWith("text/") || file.type.includes("json") || file.type.includes("xml") || file.type.includes("javascript") || file.type.includes("yaml"))) {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
const name = file.name ? file.name.toLowerCase() : "";
|
|
944
|
+
return /\.(txt|md|markdown|json|js|mjs|cjs|ts|tsx|jsx|py|pyw|rb|php|java|c|cpp|cc|cxx|h|hpp|rs|go|sh|bash|zsh|sql|html|htm|css|scss|sass|less|vue|svelte|yaml|yml|toml|ini|env|xml|log|csv|tsv|diff|patch|dockerfile|makefile)$/i.test(name);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
async function handleIncomingFiles(files) {
|
|
948
|
+
if (!files || files.length === 0) return;
|
|
949
|
+
const list = Array.from(files);
|
|
950
|
+
|
|
951
|
+
let imageCount = 0;
|
|
952
|
+
let textCount = 0;
|
|
953
|
+
|
|
954
|
+
for (const file of list) {
|
|
955
|
+
const isImg = (file.type && file.type.startsWith("image/")) ||
|
|
956
|
+
(file.name && /\.(png|jpe?g|webp|gif|bmp|svg|ico|avif|heic|heif)$/i.test(file.name));
|
|
957
|
+
|
|
958
|
+
if (isImg) {
|
|
959
|
+
try {
|
|
960
|
+
const imgObj = await processImageFile(file);
|
|
961
|
+
if (imgObj) {
|
|
962
|
+
state.attachedImages.push(imgObj);
|
|
963
|
+
renderImagePreviews();
|
|
964
|
+
imageCount++;
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
console.error("Failed to process image file:", err);
|
|
968
|
+
}
|
|
969
|
+
} else if (isTextFile(file)) {
|
|
970
|
+
if (file.size > 1024 * 1024) {
|
|
971
|
+
showToast(`文件 ${file.name} 较大,建议放入工作目录供 pi 访问`);
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
const content = await new Promise((resolve, reject) => {
|
|
976
|
+
const reader = new FileReader();
|
|
977
|
+
reader.onload = () => resolve(reader.result);
|
|
978
|
+
reader.onerror = reject;
|
|
979
|
+
reader.readAsText(file);
|
|
980
|
+
});
|
|
981
|
+
if (typeof content === "string") {
|
|
982
|
+
const lang = getLanguageFromFilename(file.name);
|
|
983
|
+
const composer = $("#composer");
|
|
984
|
+
if (composer) {
|
|
985
|
+
const block = `[附件: ${file.name}]\n\`\`\`${lang}\n${content}\n\`\`\`\n`;
|
|
986
|
+
if (composer.value.trim()) {
|
|
987
|
+
composer.value = composer.value.trimEnd() + "\n\n" + block;
|
|
988
|
+
} else {
|
|
989
|
+
composer.value = block;
|
|
990
|
+
}
|
|
991
|
+
autoResize();
|
|
992
|
+
composer.focus();
|
|
993
|
+
textCount++;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
} catch (err) {
|
|
997
|
+
console.error("Failed to read text file:", err);
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
showToast(`暂不支持直接解析该附件格式 (${file.name || "未知类型"})`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (imageCount > 0) {
|
|
1005
|
+
showToast(`已添加 ${imageCount} 张图片附件`);
|
|
1006
|
+
}
|
|
1007
|
+
if (textCount > 0) {
|
|
1008
|
+
showToast(`已导入 ${textCount} 个文本文件`);
|
|
1009
|
+
}
|
|
873
1010
|
}
|
|
874
1011
|
|
|
1012
|
+
const handleImageFiles = handleIncomingFiles;
|
|
1013
|
+
|
|
875
1014
|
function exportCurrentSession() {
|
|
876
1015
|
const chatInner = $("#chat-inner");
|
|
877
1016
|
if (!chatInner || chatInner.children.length === 0) {
|
|
@@ -2335,6 +2474,21 @@ function baseName(p) {
|
|
|
2335
2474
|
}
|
|
2336
2475
|
|
|
2337
2476
|
// ---- Composer ----
|
|
2477
|
+
function getComposerPlaceholder() {
|
|
2478
|
+
const isMobile = window.innerWidth <= 768;
|
|
2479
|
+
if (state.aborting) {
|
|
2480
|
+
return isMobile ? "正在中止…" : "正在中止当前任务…";
|
|
2481
|
+
}
|
|
2482
|
+
if (state.streaming) {
|
|
2483
|
+
return isMobile
|
|
2484
|
+
? "AI 运行中… 可输入补充指令"
|
|
2485
|
+
: "AI 正在运行中… 可在此输入补充/指导指令,按 Enter 或点击【插入指令】实时调整";
|
|
2486
|
+
}
|
|
2487
|
+
return isMobile
|
|
2488
|
+
? "给 pi 发消息…"
|
|
2489
|
+
: "给 pi 发消息… (Enter 发送,Shift+Enter 换行)";
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2338
2492
|
function updateComposerUI() {
|
|
2339
2493
|
const ta = $("#composer");
|
|
2340
2494
|
const text = ta ? ta.value.trim() : "";
|
|
@@ -2356,7 +2510,7 @@ function updateComposerUI() {
|
|
|
2356
2510
|
if (steerBtn) {
|
|
2357
2511
|
steerBtn.style.display = "none";
|
|
2358
2512
|
}
|
|
2359
|
-
if (ta) ta.placeholder =
|
|
2513
|
+
if (ta) ta.placeholder = getComposerPlaceholder();
|
|
2360
2514
|
} else if (state.streaming) {
|
|
2361
2515
|
if (sendBtn) {
|
|
2362
2516
|
sendBtn.classList.add("stop");
|
|
@@ -2372,7 +2526,7 @@ function updateComposerUI() {
|
|
|
2372
2526
|
steerBtn.style.display = "none";
|
|
2373
2527
|
}
|
|
2374
2528
|
}
|
|
2375
|
-
if (ta) ta.placeholder =
|
|
2529
|
+
if (ta) ta.placeholder = getComposerPlaceholder();
|
|
2376
2530
|
} else {
|
|
2377
2531
|
if (sendBtn) {
|
|
2378
2532
|
sendBtn.classList.remove("stop");
|
|
@@ -2383,7 +2537,7 @@ function updateComposerUI() {
|
|
|
2383
2537
|
if (steerBtn) {
|
|
2384
2538
|
steerBtn.style.display = "none";
|
|
2385
2539
|
}
|
|
2386
|
-
if (ta) ta.placeholder =
|
|
2540
|
+
if (ta) ta.placeholder = getComposerPlaceholder();
|
|
2387
2541
|
}
|
|
2388
2542
|
}
|
|
2389
2543
|
|
|
@@ -2699,30 +2853,43 @@ async function init() {
|
|
|
2699
2853
|
exportBtn.addEventListener("click", exportCurrentSession);
|
|
2700
2854
|
}
|
|
2701
2855
|
|
|
2702
|
-
// Image attach /
|
|
2856
|
+
// Image and file attach / picker / paste / drag-and-drop
|
|
2703
2857
|
const btnAttach = $("#btnAttachImage");
|
|
2704
2858
|
const fileInput = $("#imageFileInput");
|
|
2705
|
-
if (
|
|
2706
|
-
btnAttach.addEventListener("click", () => fileInput.click());
|
|
2859
|
+
if (fileInput) {
|
|
2707
2860
|
fileInput.addEventListener("change", (e) => {
|
|
2708
|
-
|
|
2861
|
+
handleIncomingFiles(e.target.files);
|
|
2709
2862
|
fileInput.value = "";
|
|
2710
2863
|
});
|
|
2711
2864
|
}
|
|
2712
2865
|
|
|
2713
|
-
|
|
2866
|
+
if (btnAttach) {
|
|
2867
|
+
// If not a label (e.g. fallback button), click input
|
|
2868
|
+
if (btnAttach.tagName !== "LABEL") {
|
|
2869
|
+
btnAttach.addEventListener("click", () => fileInput?.click());
|
|
2870
|
+
}
|
|
2871
|
+
// Keyboard accessibility for Enter / Space
|
|
2872
|
+
btnAttach.addEventListener("keydown", (e) => {
|
|
2873
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2874
|
+
e.preventDefault();
|
|
2875
|
+
fileInput?.click();
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
// Image and file paste support
|
|
2714
2881
|
document.addEventListener("paste", (e) => {
|
|
2715
2882
|
const items = e.clipboardData?.items;
|
|
2716
2883
|
if (!items) return;
|
|
2717
2884
|
const files = [];
|
|
2718
2885
|
for (let i = 0; i < items.length; i++) {
|
|
2719
|
-
if (items[i].
|
|
2886
|
+
if (items[i].kind === "file") {
|
|
2720
2887
|
const file = items[i].getAsFile();
|
|
2721
2888
|
if (file) files.push(file);
|
|
2722
2889
|
}
|
|
2723
2890
|
}
|
|
2724
2891
|
if (files.length > 0) {
|
|
2725
|
-
|
|
2892
|
+
handleIncomingFiles(files);
|
|
2726
2893
|
const ta = $("#composer");
|
|
2727
2894
|
if (ta) ta.focus();
|
|
2728
2895
|
}
|
|
@@ -2732,7 +2899,7 @@ async function init() {
|
|
|
2732
2899
|
window.addEventListener("dragover", (e) => e.preventDefault(), false);
|
|
2733
2900
|
window.addEventListener("drop", (e) => e.preventDefault(), false);
|
|
2734
2901
|
|
|
2735
|
-
// Drag and drop images to composer
|
|
2902
|
+
// Drag and drop images and files to composer
|
|
2736
2903
|
const composerBox = $("#composerInner") || $(".composer");
|
|
2737
2904
|
if (composerBox) {
|
|
2738
2905
|
composerBox.addEventListener("dragover", (e) => {
|
|
@@ -2750,7 +2917,7 @@ async function init() {
|
|
|
2750
2917
|
e.stopPropagation();
|
|
2751
2918
|
composerBox.classList.remove("drag-over");
|
|
2752
2919
|
if (e.dataTransfer?.files) {
|
|
2753
|
-
|
|
2920
|
+
handleIncomingFiles(e.dataTransfer.files);
|
|
2754
2921
|
}
|
|
2755
2922
|
});
|
|
2756
2923
|
}
|
|
@@ -2824,6 +2991,7 @@ async function init() {
|
|
|
2824
2991
|
lastIsMobile = isMobile;
|
|
2825
2992
|
// Crossing the breakpoint: always close the drawer to reach a known state.
|
|
2826
2993
|
$(".app").classList.remove("sidebar-open");
|
|
2994
|
+
updateComposerUI();
|
|
2827
2995
|
}
|
|
2828
2996
|
});
|
|
2829
2997
|
|
package/public/index.html
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
<div id="emptyState" class="empty-state" style="display:flex;">
|
|
80
80
|
<div class="logo">π</div>
|
|
81
81
|
<h1>有什么可以帮你?</h1>
|
|
82
|
-
<p>我是 <strong>pi</strong>
|
|
82
|
+
<p>我是 <strong>pi</strong> 编程助手,可以读写文件、运行命令、改代码。在下方输入你的需求开始,或打开历史会话继续。</p>
|
|
83
83
|
<div class="empty-model-banner" id="emptyModelBanner">
|
|
84
84
|
<div class="empty-model-left">
|
|
85
85
|
<span class="empty-model-icon">🤖</span>
|
|
@@ -106,13 +106,13 @@
|
|
|
106
106
|
<div class="composer">
|
|
107
107
|
<div class="image-preview-bar" id="imagePreviewBar" style="display:none;"></div>
|
|
108
108
|
<div class="composer-inner" id="composerInner">
|
|
109
|
-
<input type="file" id="imageFileInput" accept="image/*,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg" multiple
|
|
110
|
-
<
|
|
109
|
+
<input type="file" id="imageFileInput" class="sr-only-file-input" accept="image/*,.png,.jpg,.jpeg,.webp,.gif,.bmp,.svg,.heic,.heif,.ico,.avif,text/*,.txt,.md,.json,.js,.ts,.py,.sh,.html,.css,.yaml,.yml,.toml,.xml,.sql,.log,.csv" multiple>
|
|
110
|
+
<label class="attach-btn" id="btnAttachImage" for="imageFileInput" role="button" tabindex="0" title="添加附件 / 图片 (支持拖拽与截图粘贴)">
|
|
111
111
|
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
112
112
|
<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
113
113
|
</svg>
|
|
114
|
-
</
|
|
115
|
-
<textarea id="composer" rows="1" placeholder="给 pi 发消息…
|
|
114
|
+
</label>
|
|
115
|
+
<textarea id="composer" rows="1" placeholder="给 pi 发消息…"></textarea>
|
|
116
116
|
<button class="steer-btn" id="steerBtn" title="在 AI 运行过程中插入指导指令" style="display:none;">
|
|
117
117
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>
|
|
118
118
|
<span>插入指令</span>
|
package/public/style.css
CHANGED
|
@@ -812,6 +812,7 @@ body {
|
|
|
812
812
|
gap: 6px;
|
|
813
813
|
font-size: 13px;
|
|
814
814
|
color: var(--text);
|
|
815
|
+
flex-wrap: wrap;
|
|
815
816
|
}
|
|
816
817
|
.empty-model-label {
|
|
817
818
|
color: var(--text-muted);
|
|
@@ -824,6 +825,20 @@ body {
|
|
|
824
825
|
text-overflow: ellipsis;
|
|
825
826
|
white-space: nowrap;
|
|
826
827
|
}
|
|
828
|
+
.badge-default {
|
|
829
|
+
display: inline-flex;
|
|
830
|
+
align-items: center;
|
|
831
|
+
font-size: 11px;
|
|
832
|
+
font-weight: 500;
|
|
833
|
+
color: #f59e0b;
|
|
834
|
+
background: rgba(245, 158, 11, 0.12);
|
|
835
|
+
border: 1px solid rgba(245, 158, 11, 0.25);
|
|
836
|
+
border-radius: 4px;
|
|
837
|
+
padding: 1px 6px;
|
|
838
|
+
white-space: nowrap !important;
|
|
839
|
+
flex-shrink: 0;
|
|
840
|
+
line-height: 1.3;
|
|
841
|
+
}
|
|
827
842
|
.btn-topbar-action {
|
|
828
843
|
display: inline-flex;
|
|
829
844
|
align-items: center;
|
|
@@ -880,6 +895,20 @@ body {
|
|
|
880
895
|
.suggestions .chip:hover { border-color: var(--text-dim); color: var(--text); }
|
|
881
896
|
|
|
882
897
|
/* ---------- Composer & Image Upload ---------- */
|
|
898
|
+
.sr-only-file-input {
|
|
899
|
+
position: absolute !important;
|
|
900
|
+
width: 1px !important;
|
|
901
|
+
height: 1px !important;
|
|
902
|
+
padding: 0 !important;
|
|
903
|
+
margin: -1px !important;
|
|
904
|
+
overflow: hidden !important;
|
|
905
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
906
|
+
white-space: nowrap !important;
|
|
907
|
+
border: 0 !important;
|
|
908
|
+
opacity: 0 !important;
|
|
909
|
+
pointer-events: none !important;
|
|
910
|
+
}
|
|
911
|
+
|
|
883
912
|
.composer {
|
|
884
913
|
padding: 12px 24px 20px;
|
|
885
914
|
background: var(--bg);
|
|
@@ -902,6 +931,12 @@ body {
|
|
|
902
931
|
background: #111;
|
|
903
932
|
flex-shrink: 0;
|
|
904
933
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
934
|
+
cursor: pointer;
|
|
935
|
+
transition: transform 0.15s ease, border-color 0.15s ease;
|
|
936
|
+
}
|
|
937
|
+
.image-preview-item:hover {
|
|
938
|
+
transform: translateY(-2px);
|
|
939
|
+
border-color: var(--accent);
|
|
905
940
|
}
|
|
906
941
|
.image-preview-item img {
|
|
907
942
|
width: 100%;
|
|
@@ -926,10 +961,12 @@ body {
|
|
|
926
961
|
font-size: 12px;
|
|
927
962
|
line-height: 1;
|
|
928
963
|
padding: 0;
|
|
929
|
-
transition: background 0.15s;
|
|
964
|
+
transition: background 0.15s, transform 0.1s;
|
|
965
|
+
z-index: 2;
|
|
930
966
|
}
|
|
931
967
|
.image-preview-remove:hover {
|
|
932
968
|
background: var(--danger, #ef4444);
|
|
969
|
+
transform: scale(1.1);
|
|
933
970
|
}
|
|
934
971
|
|
|
935
972
|
.composer-inner {
|
|
@@ -957,12 +994,15 @@ body {
|
|
|
957
994
|
border: none;
|
|
958
995
|
color: var(--text-muted);
|
|
959
996
|
cursor: pointer;
|
|
960
|
-
display: flex;
|
|
997
|
+
display: inline-flex;
|
|
961
998
|
align-items: center;
|
|
962
999
|
justify-content: center;
|
|
963
1000
|
border-radius: 50%;
|
|
964
1001
|
transition: color 0.15s ease, background 0.15s ease, transform 0.1s ease;
|
|
965
1002
|
flex-shrink: 0;
|
|
1003
|
+
-webkit-tap-highlight-color: transparent;
|
|
1004
|
+
touch-action: manipulation;
|
|
1005
|
+
user-select: none;
|
|
966
1006
|
}
|
|
967
1007
|
.composer .attach-btn:hover {
|
|
968
1008
|
color: var(--text);
|
|
@@ -1504,7 +1544,7 @@ body {
|
|
|
1504
1544
|
|
|
1505
1545
|
/* Topbar Mobile Adjustments — fixed, taller, shadowed so it is always findable */
|
|
1506
1546
|
.topbar {
|
|
1507
|
-
height:
|
|
1547
|
+
height: 50px;
|
|
1508
1548
|
padding: 0 8px;
|
|
1509
1549
|
gap: 6px;
|
|
1510
1550
|
position: fixed;
|
|
@@ -1515,21 +1555,16 @@ body {
|
|
|
1515
1555
|
background: var(--bg);
|
|
1516
1556
|
border-bottom: 1px solid var(--border);
|
|
1517
1557
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
|
1558
|
+
display: flex;
|
|
1559
|
+
align-items: center;
|
|
1518
1560
|
}
|
|
1519
1561
|
|
|
1520
1562
|
.main {
|
|
1521
|
-
padding-top:
|
|
1563
|
+
padding-top: 50px;
|
|
1522
1564
|
}
|
|
1523
1565
|
|
|
1524
1566
|
.topbar .session-name {
|
|
1525
|
-
|
|
1526
|
-
font-weight: 500;
|
|
1527
|
-
min-width: 0;
|
|
1528
|
-
flex: 1;
|
|
1529
|
-
overflow: hidden;
|
|
1530
|
-
text-overflow: ellipsis;
|
|
1531
|
-
white-space: nowrap;
|
|
1532
|
-
padding: 0 2px;
|
|
1567
|
+
display: none !important;
|
|
1533
1568
|
}
|
|
1534
1569
|
|
|
1535
1570
|
.btn-toggle-sidebar {
|
|
@@ -1548,7 +1583,7 @@ body {
|
|
|
1548
1583
|
#cwdPillWrap {
|
|
1549
1584
|
padding: 4px 7px;
|
|
1550
1585
|
font-size: 11px;
|
|
1551
|
-
max-width:
|
|
1586
|
+
max-width: 80px;
|
|
1552
1587
|
overflow: hidden;
|
|
1553
1588
|
text-overflow: ellipsis;
|
|
1554
1589
|
white-space: nowrap;
|
|
@@ -1559,22 +1594,26 @@ body {
|
|
|
1559
1594
|
display: flex;
|
|
1560
1595
|
align-items: center;
|
|
1561
1596
|
gap: 4px;
|
|
1562
|
-
flex
|
|
1563
|
-
|
|
1597
|
+
flex: 1;
|
|
1598
|
+
min-width: 0;
|
|
1564
1599
|
}
|
|
1565
1600
|
|
|
1566
1601
|
.model-pill {
|
|
1567
1602
|
padding: 4px 8px;
|
|
1568
1603
|
font-size: 11px;
|
|
1569
|
-
max-width:
|
|
1604
|
+
max-width: 125px;
|
|
1570
1605
|
overflow: hidden;
|
|
1571
1606
|
text-overflow: ellipsis;
|
|
1572
1607
|
white-space: nowrap;
|
|
1573
|
-
flex-shrink:
|
|
1608
|
+
flex-shrink: 1;
|
|
1609
|
+
min-width: 0;
|
|
1574
1610
|
gap: 4px;
|
|
1575
1611
|
}
|
|
1576
1612
|
.model-pill-name {
|
|
1577
|
-
max-width:
|
|
1613
|
+
max-width: 100%;
|
|
1614
|
+
overflow: hidden;
|
|
1615
|
+
text-overflow: ellipsis;
|
|
1616
|
+
white-space: nowrap;
|
|
1578
1617
|
}
|
|
1579
1618
|
.model-pill-badge {
|
|
1580
1619
|
display: none !important;
|
|
@@ -1597,20 +1636,30 @@ body {
|
|
|
1597
1636
|
font-weight: 500;
|
|
1598
1637
|
}
|
|
1599
1638
|
|
|
1639
|
+
.btn-topbar-action {
|
|
1640
|
+
width: 32px;
|
|
1641
|
+
height: 32px;
|
|
1642
|
+
padding: 0;
|
|
1643
|
+
display: inline-flex;
|
|
1644
|
+
align-items: center;
|
|
1645
|
+
justify-content: center;
|
|
1646
|
+
flex-shrink: 0;
|
|
1647
|
+
}
|
|
1648
|
+
.topbar-action-label {
|
|
1649
|
+
display: none !important;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1600
1652
|
@media (max-width: 360px) {
|
|
1601
1653
|
.topbar {
|
|
1602
1654
|
padding: 0 4px;
|
|
1603
1655
|
gap: 4px;
|
|
1604
1656
|
}
|
|
1605
1657
|
#cwdPillWrap {
|
|
1606
|
-
max-width:
|
|
1658
|
+
max-width: 60px;
|
|
1607
1659
|
padding: 4px 5px;
|
|
1608
1660
|
}
|
|
1609
1661
|
.model-pill {
|
|
1610
|
-
max-width:
|
|
1611
|
-
}
|
|
1612
|
-
.model-pill-name {
|
|
1613
|
-
max-width: 50px;
|
|
1662
|
+
max-width: 95px;
|
|
1614
1663
|
}
|
|
1615
1664
|
.thinking-pill {
|
|
1616
1665
|
padding: 4px 5px;
|
|
@@ -1637,13 +1686,48 @@ body {
|
|
|
1637
1686
|
}
|
|
1638
1687
|
|
|
1639
1688
|
.empty-model-banner {
|
|
1640
|
-
padding:
|
|
1641
|
-
flex-direction:
|
|
1689
|
+
padding: 12px 14px;
|
|
1690
|
+
flex-direction: row;
|
|
1691
|
+
align-items: center;
|
|
1692
|
+
justify-content: space-between;
|
|
1693
|
+
gap: 10px;
|
|
1694
|
+
width: 100%;
|
|
1695
|
+
max-width: 100%;
|
|
1696
|
+
box-sizing: border-box;
|
|
1697
|
+
}
|
|
1698
|
+
.empty-model-left {
|
|
1699
|
+
display: flex;
|
|
1642
1700
|
align-items: flex-start;
|
|
1643
|
-
gap:
|
|
1701
|
+
gap: 10px;
|
|
1702
|
+
flex: 1;
|
|
1703
|
+
min-width: 0;
|
|
1704
|
+
}
|
|
1705
|
+
.empty-model-icon {
|
|
1706
|
+
font-size: 22px;
|
|
1707
|
+
margin-top: 1px;
|
|
1708
|
+
flex-shrink: 0;
|
|
1709
|
+
}
|
|
1710
|
+
.empty-model-info {
|
|
1711
|
+
flex: 1;
|
|
1712
|
+
min-width: 0;
|
|
1713
|
+
gap: 4px;
|
|
1714
|
+
}
|
|
1715
|
+
.empty-model-title-row {
|
|
1716
|
+
flex-wrap: wrap;
|
|
1717
|
+
gap: 4px 6px;
|
|
1718
|
+
font-size: 12.5px;
|
|
1719
|
+
line-height: 1.4;
|
|
1720
|
+
}
|
|
1721
|
+
.empty-model-name {
|
|
1722
|
+
font-weight: 600;
|
|
1723
|
+
word-break: break-all;
|
|
1724
|
+
white-space: normal;
|
|
1644
1725
|
}
|
|
1645
1726
|
.btn-change-model {
|
|
1646
|
-
align-self:
|
|
1727
|
+
align-self: center;
|
|
1728
|
+
padding: 6px 10px;
|
|
1729
|
+
font-size: 11.5px;
|
|
1730
|
+
flex-shrink: 0;
|
|
1647
1731
|
}
|
|
1648
1732
|
|
|
1649
1733
|
.mobile-toolbar-fab {
|
|
@@ -1691,7 +1775,7 @@ body {
|
|
|
1691
1775
|
|
|
1692
1776
|
/* Empty state mobile adjustments */
|
|
1693
1777
|
.empty-state {
|
|
1694
|
-
padding:
|
|
1778
|
+
padding: 20px 14px;
|
|
1695
1779
|
gap: 12px;
|
|
1696
1780
|
}
|
|
1697
1781
|
|
|
@@ -1709,31 +1793,69 @@ body {
|
|
|
1709
1793
|
}
|
|
1710
1794
|
|
|
1711
1795
|
.suggestions {
|
|
1796
|
+
display: flex;
|
|
1712
1797
|
gap: 8px;
|
|
1713
|
-
margin-top:
|
|
1798
|
+
margin-top: 10px;
|
|
1799
|
+
flex-wrap: wrap;
|
|
1800
|
+
justify-content: center;
|
|
1801
|
+
width: 100%;
|
|
1802
|
+
max-width: 100%;
|
|
1803
|
+
box-sizing: border-box;
|
|
1714
1804
|
}
|
|
1715
1805
|
|
|
1716
1806
|
.suggestions .chip {
|
|
1717
|
-
padding:
|
|
1807
|
+
padding: 8px 12px;
|
|
1718
1808
|
font-size: 12px;
|
|
1809
|
+
line-height: 1.35;
|
|
1810
|
+
text-align: center;
|
|
1811
|
+
border-radius: 18px;
|
|
1812
|
+
max-width: 100%;
|
|
1813
|
+
box-sizing: border-box;
|
|
1814
|
+
white-space: normal;
|
|
1719
1815
|
}
|
|
1720
1816
|
|
|
1721
1817
|
/* Composer input mobile adjustments */
|
|
1722
1818
|
.composer {
|
|
1723
|
-
padding: 8px 12px
|
|
1819
|
+
padding: 8px 12px;
|
|
1820
|
+
padding-bottom: max(12px, calc(8px + env(safe-area-inset-bottom, 0px)));
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
.image-preview-bar {
|
|
1824
|
+
gap: 8px;
|
|
1825
|
+
margin-bottom: 6px;
|
|
1826
|
+
padding: 2px 4px;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
.image-preview-item {
|
|
1830
|
+
width: 60px;
|
|
1831
|
+
height: 60px;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
.image-preview-remove {
|
|
1835
|
+
width: 22px;
|
|
1836
|
+
height: 22px;
|
|
1837
|
+
font-size: 14px;
|
|
1838
|
+
top: 2px;
|
|
1839
|
+
right: 2px;
|
|
1840
|
+
background: rgba(0, 0, 0, 0.85);
|
|
1724
1841
|
}
|
|
1725
1842
|
|
|
1726
1843
|
.composer-inner {
|
|
1727
|
-
padding:
|
|
1844
|
+
padding: 5px 8px 5px 8px;
|
|
1728
1845
|
border-radius: 22px;
|
|
1729
1846
|
gap: 6px;
|
|
1730
1847
|
}
|
|
1731
1848
|
|
|
1849
|
+
.composer .attach-btn {
|
|
1850
|
+
width: 32px;
|
|
1851
|
+
height: 32px;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1732
1854
|
.composer textarea {
|
|
1733
1855
|
padding: 6px 0;
|
|
1734
|
-
font-size:
|
|
1856
|
+
font-size: 15px;
|
|
1735
1857
|
line-height: 20px;
|
|
1736
|
-
min-height:
|
|
1858
|
+
min-height: 28px;
|
|
1737
1859
|
max-height: 160px;
|
|
1738
1860
|
}
|
|
1739
1861
|
|