|
|
@@ -125,6 +125,20 @@
|
|
|
text-align: left;
|
|
|
}
|
|
|
|
|
|
+ .ai-card__image {
|
|
|
+ display: block;
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ margin: 8px 0;
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .user-bubble__image {
|
|
|
+ display: block;
|
|
|
+ max-width: 180px;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
.fallback-card {
|
|
|
margin: 14px 15px 0;
|
|
|
padding: 14px 13px;
|
|
|
@@ -314,6 +328,82 @@
|
|
|
.replace(/"/g, """);
|
|
|
}
|
|
|
|
|
|
+ var IMAGE_EXT = "(?:jpe?g|png|gif|webp|bmp|avif)";
|
|
|
+ var IMAGE_URL_PATTERN =
|
|
|
+ "https?:\\/\\/[^\\s))\\]]+\\." + IMAGE_EXT + "(?:\\?[^\\s))\\]]*)?";
|
|
|
+
|
|
|
+ function isInsideImageUrlSyntax(full, offset) {
|
|
|
+ if (offset >= 2 && full.slice(offset - 2, offset) === "](") return true;
|
|
|
+ if (offset >= 1 && full[offset - 1] === "(") return true;
|
|
|
+ if (offset >= 6 && full.slice(offset - 6, offset) === "{{IMG|") return true;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** AI 回复常见:店名(https://...jpg) — 转为可渲染的图片占位 */
|
|
|
+ function embedInlineImageUrls(text) {
|
|
|
+ if (text == null) return "";
|
|
|
+ var t = String(text);
|
|
|
+
|
|
|
+ t = t.replace(
|
|
|
+ new RegExp("([^\\n((]+?)\\((" + IMAGE_URL_PATTERN + ")\\)", "gi"),
|
|
|
+ function (_, label, url) {
|
|
|
+ var name = String(label).trim();
|
|
|
+ if (!name) return "\n\n{{IMG|" + url + "|}}\n\n";
|
|
|
+ var alt = name.length > 24 ? "图片" : name.replace(/[\[\]]/g, "");
|
|
|
+ return name + "\n\n{{IMG|" + url + "|" + alt + "}}\n\n";
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ t = t.replace(
|
|
|
+ new RegExp("\\((" + IMAGE_URL_PATTERN + ")\\)", "gi"),
|
|
|
+ function (_, url) {
|
|
|
+ return "\n\n{{IMG|" + url + "|}}\n\n";
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ t = t.replace(new RegExp(IMAGE_URL_PATTERN, "gi"), function (url, offset, full) {
|
|
|
+ if (isInsideImageUrlSyntax(full, offset)) return url;
|
|
|
+ return "\n\n{{IMG|" + url + "|}}\n\n";
|
|
|
+ });
|
|
|
+
|
|
|
+ return t.replace(/\n{3,}/g, "\n\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ function formatTextBlockHtml(text) {
|
|
|
+ return escHtml(text).replace(/\n/g, "<br>");
|
|
|
+ }
|
|
|
+
|
|
|
+ function buildAiImageTag(url, alt) {
|
|
|
+ var safeUrl = escHtml(url);
|
|
|
+ var safeAlt = escHtml(alt || "图片");
|
|
|
+ return (
|
|
|
+ '<img class="ai-card__image" src="' +
|
|
|
+ safeUrl +
|
|
|
+ '" alt="' +
|
|
|
+ safeAlt +
|
|
|
+ '" loading="lazy" decoding="async" />'
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderAiContentHtml(text) {
|
|
|
+ var t = embedInlineImageUrls(text);
|
|
|
+ var html = "";
|
|
|
+ var tokenRe = /\{\{IMG\|([^|]+)\|([^}]*)\}\}/g;
|
|
|
+ var lastIndex = 0;
|
|
|
+ var match;
|
|
|
+ while ((match = tokenRe.exec(t)) !== null) {
|
|
|
+ if (match.index > lastIndex) {
|
|
|
+ html += formatTextBlockHtml(t.slice(lastIndex, match.index));
|
|
|
+ }
|
|
|
+ html += buildAiImageTag(match[1], match[2]);
|
|
|
+ lastIndex = tokenRe.lastIndex;
|
|
|
+ }
|
|
|
+ if (lastIndex < t.length) {
|
|
|
+ html += formatTextBlockHtml(t.slice(lastIndex));
|
|
|
+ }
|
|
|
+ return html;
|
|
|
+ }
|
|
|
+
|
|
|
function q(name) {
|
|
|
try {
|
|
|
var v = new URLSearchParams(location.search || "").get(name);
|
|
|
@@ -375,7 +465,14 @@
|
|
|
var msg = list[i];
|
|
|
if (!msg) continue;
|
|
|
if (msg.role === "user") {
|
|
|
- var userText = msg.isImage ? "[图片]" : String(msg.content || "").trim();
|
|
|
+ if (msg.isImage && msg.imageUrl) {
|
|
|
+ html +=
|
|
|
+ '<div class="row row--user"><div class="user-bubble"><img class="user-bubble__image" src="' +
|
|
|
+ escHtml(msg.imageUrl) +
|
|
|
+ '" alt="图片" loading="lazy" decoding="async" /></div></div>';
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ var userText = String(msg.content || "").trim();
|
|
|
if (!userText) continue;
|
|
|
html +=
|
|
|
'<div class="row row--user"><div class="user-bubble">' +
|
|
|
@@ -388,7 +485,7 @@
|
|
|
if (!aiText) continue;
|
|
|
html +=
|
|
|
'<div class="row row--ai"><div class="ai-card"><div class="ai-card__text">' +
|
|
|
- escHtml(aiText) +
|
|
|
+ renderAiContentHtml(aiText) +
|
|
|
"</div></div></div>";
|
|
|
}
|
|
|
}
|