@liguoshuai/pi-web-chat 1.10.2 → 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 +12 -0
- package/package.json +1 -1
- package/public/app.js +185 -33
- package/public/index.html +3 -3
- package/public/style.css +53 -3
package/docs/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ 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
|
+
|
|
10
22
|
## [1.10.2] - 2026-08-30
|
|
11
23
|
|
|
12
24
|
### Fixed & 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) {
|
|
@@ -2714,30 +2853,43 @@ async function init() {
|
|
|
2714
2853
|
exportBtn.addEventListener("click", exportCurrentSession);
|
|
2715
2854
|
}
|
|
2716
2855
|
|
|
2717
|
-
// Image attach /
|
|
2856
|
+
// Image and file attach / picker / paste / drag-and-drop
|
|
2718
2857
|
const btnAttach = $("#btnAttachImage");
|
|
2719
2858
|
const fileInput = $("#imageFileInput");
|
|
2720
|
-
if (
|
|
2721
|
-
btnAttach.addEventListener("click", () => fileInput.click());
|
|
2859
|
+
if (fileInput) {
|
|
2722
2860
|
fileInput.addEventListener("change", (e) => {
|
|
2723
|
-
|
|
2861
|
+
handleIncomingFiles(e.target.files);
|
|
2724
2862
|
fileInput.value = "";
|
|
2725
2863
|
});
|
|
2726
2864
|
}
|
|
2727
2865
|
|
|
2728
|
-
|
|
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
|
|
2729
2881
|
document.addEventListener("paste", (e) => {
|
|
2730
2882
|
const items = e.clipboardData?.items;
|
|
2731
2883
|
if (!items) return;
|
|
2732
2884
|
const files = [];
|
|
2733
2885
|
for (let i = 0; i < items.length; i++) {
|
|
2734
|
-
if (items[i].
|
|
2886
|
+
if (items[i].kind === "file") {
|
|
2735
2887
|
const file = items[i].getAsFile();
|
|
2736
2888
|
if (file) files.push(file);
|
|
2737
2889
|
}
|
|
2738
2890
|
}
|
|
2739
2891
|
if (files.length > 0) {
|
|
2740
|
-
|
|
2892
|
+
handleIncomingFiles(files);
|
|
2741
2893
|
const ta = $("#composer");
|
|
2742
2894
|
if (ta) ta.focus();
|
|
2743
2895
|
}
|
|
@@ -2747,7 +2899,7 @@ async function init() {
|
|
|
2747
2899
|
window.addEventListener("dragover", (e) => e.preventDefault(), false);
|
|
2748
2900
|
window.addEventListener("drop", (e) => e.preventDefault(), false);
|
|
2749
2901
|
|
|
2750
|
-
// Drag and drop images to composer
|
|
2902
|
+
// Drag and drop images and files to composer
|
|
2751
2903
|
const composerBox = $("#composerInner") || $(".composer");
|
|
2752
2904
|
if (composerBox) {
|
|
2753
2905
|
composerBox.addEventListener("dragover", (e) => {
|
|
@@ -2765,7 +2917,7 @@ async function init() {
|
|
|
2765
2917
|
e.stopPropagation();
|
|
2766
2918
|
composerBox.classList.remove("drag-over");
|
|
2767
2919
|
if (e.dataTransfer?.files) {
|
|
2768
|
-
|
|
2920
|
+
handleIncomingFiles(e.dataTransfer.files);
|
|
2769
2921
|
}
|
|
2770
2922
|
});
|
|
2771
2923
|
}
|
package/public/index.html
CHANGED
|
@@ -106,12 +106,12 @@
|
|
|
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
|
-
</
|
|
114
|
+
</label>
|
|
115
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>
|
package/public/style.css
CHANGED
|
@@ -895,6 +895,20 @@ body {
|
|
|
895
895
|
.suggestions .chip:hover { border-color: var(--text-dim); color: var(--text); }
|
|
896
896
|
|
|
897
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
|
+
|
|
898
912
|
.composer {
|
|
899
913
|
padding: 12px 24px 20px;
|
|
900
914
|
background: var(--bg);
|
|
@@ -917,6 +931,12 @@ body {
|
|
|
917
931
|
background: #111;
|
|
918
932
|
flex-shrink: 0;
|
|
919
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);
|
|
920
940
|
}
|
|
921
941
|
.image-preview-item img {
|
|
922
942
|
width: 100%;
|
|
@@ -941,10 +961,12 @@ body {
|
|
|
941
961
|
font-size: 12px;
|
|
942
962
|
line-height: 1;
|
|
943
963
|
padding: 0;
|
|
944
|
-
transition: background 0.15s;
|
|
964
|
+
transition: background 0.15s, transform 0.1s;
|
|
965
|
+
z-index: 2;
|
|
945
966
|
}
|
|
946
967
|
.image-preview-remove:hover {
|
|
947
968
|
background: var(--danger, #ef4444);
|
|
969
|
+
transform: scale(1.1);
|
|
948
970
|
}
|
|
949
971
|
|
|
950
972
|
.composer-inner {
|
|
@@ -972,12 +994,15 @@ body {
|
|
|
972
994
|
border: none;
|
|
973
995
|
color: var(--text-muted);
|
|
974
996
|
cursor: pointer;
|
|
975
|
-
display: flex;
|
|
997
|
+
display: inline-flex;
|
|
976
998
|
align-items: center;
|
|
977
999
|
justify-content: center;
|
|
978
1000
|
border-radius: 50%;
|
|
979
1001
|
transition: color 0.15s ease, background 0.15s ease, transform 0.1s ease;
|
|
980
1002
|
flex-shrink: 0;
|
|
1003
|
+
-webkit-tap-highlight-color: transparent;
|
|
1004
|
+
touch-action: manipulation;
|
|
1005
|
+
user-select: none;
|
|
981
1006
|
}
|
|
982
1007
|
.composer .attach-btn:hover {
|
|
983
1008
|
color: var(--text);
|
|
@@ -1795,12 +1820,37 @@ body {
|
|
|
1795
1820
|
padding-bottom: max(12px, calc(8px + env(safe-area-inset-bottom, 0px)));
|
|
1796
1821
|
}
|
|
1797
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);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1798
1843
|
.composer-inner {
|
|
1799
|
-
padding: 5px 8px 5px
|
|
1844
|
+
padding: 5px 8px 5px 8px;
|
|
1800
1845
|
border-radius: 22px;
|
|
1801
1846
|
gap: 6px;
|
|
1802
1847
|
}
|
|
1803
1848
|
|
|
1849
|
+
.composer .attach-btn {
|
|
1850
|
+
width: 32px;
|
|
1851
|
+
height: 32px;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1804
1854
|
.composer textarea {
|
|
1805
1855
|
padding: 6px 0;
|
|
1806
1856
|
font-size: 15px;
|