增加了图片上传解析的功能
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"liveServer.settings.port": 5501
|
||||||
|
}
|
||||||
8
config.json
Normal file
8
config.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"enableVision": true,
|
||||||
|
"imageConfig": {
|
||||||
|
"maxCount": 4,
|
||||||
|
"maxSizeMB": 4,
|
||||||
|
"allowedTypes": ["image/jpeg", "image/png", "image/webp", "image/gif"]
|
||||||
|
}
|
||||||
|
}
|
||||||
210
css/style.css
210
css/style.css
@@ -489,3 +489,213 @@ iconify-icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#dmermaidSvg{ height: 0px;}
|
#dmermaidSvg{ height: 0px;}
|
||||||
|
|
||||||
|
/* ========== 图片上传相关样式 ========== */
|
||||||
|
|
||||||
|
/* 图片上传按钮 */
|
||||||
|
.image-upload-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:hover {
|
||||||
|
background: rgba(6, 182, 212, 0.1);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-upload-btn:disabled:hover {
|
||||||
|
background: transparent;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片缩略图容器 */
|
||||||
|
#image-preview-container {
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 2px dashed #d1d5db;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-container:not(.hidden) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单个图片缩略图项 */
|
||||||
|
.image-thumbnail-item {
|
||||||
|
position: relative;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
background: #f9fafb;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-item:hover {
|
||||||
|
border-color: #06b6d4;
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片删除按钮 */
|
||||||
|
.image-thumbnail-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(220, 38, 38, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-item:hover .image-thumbnail-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-delete:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumbnail-delete iconify-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片数量限制提示 */
|
||||||
|
.image-count-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 2px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片预览模态窗 */
|
||||||
|
.image-preview-modal-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 90vw;
|
||||||
|
height: 90vh;
|
||||||
|
background: rgba(0, 0, 0, 0.95);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview-modal-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 聊天气泡中的图片显示 */
|
||||||
|
.chat-bubble-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble-image:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片加载中状态 */
|
||||||
|
.image-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-loading::after {
|
||||||
|
content: '';
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid #d1d5db;
|
||||||
|
border-top-color: #06b6d4;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片上传拖拽区域高亮 */
|
||||||
|
.image-drop-active {
|
||||||
|
background: rgba(6, 182, 212, 0.1) !important;
|
||||||
|
border-color: #06b6d4 !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片大小/格式错误提示 */
|
||||||
|
.image-error-toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #dc2626;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: toast-in 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toast-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
container_name: product-canvas-app
|
container_name: product-canvas-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# 是否启用图片解析功能(Vision API),默认启用
|
||||||
|
- ENABLE_VISION=true
|
||||||
|
volumes:
|
||||||
|
# 可选:挂载自定义配置文件覆盖默认配置
|
||||||
|
# - ./config.json:/usr/share/nginx/html/config.json:ro
|
||||||
41
index.html
41
index.html
@@ -65,10 +65,19 @@
|
|||||||
|
|
||||||
<!-- 输入区 -->
|
<!-- 输入区 -->
|
||||||
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
<div class="p-3 border-t-3 border-gray-300 bg-yellow-50">
|
||||||
|
<!-- 图片缩略图预览区 -->
|
||||||
|
<div id="image-preview-container" class="mb-2 flex flex-wrap gap-2 hidden">
|
||||||
|
<!-- 动态插入图片缩略图 -->
|
||||||
|
</div>
|
||||||
<div class="relative flex items-center gap-2">
|
<div class="relative flex items-center gap-2">
|
||||||
|
<!-- 图片上传按钮 -->
|
||||||
|
<button id="image-upload-btn" class="image-upload-btn text-gray-500 hover:text-cyan-600 transition-colors p-2" title="上传图片 (支持粘贴)">
|
||||||
|
<iconify-icon icon="ph:image-bold" class="text-2xl"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
<input type="file" id="image-file-input" accept="image/jpeg,image/png,image/webp,image/gif" multiple class="hidden" />
|
||||||
<textarea
|
<textarea
|
||||||
id="chat-input"
|
id="chat-input"
|
||||||
placeholder="输入您的想法,按Enter发送,Shift+Enter换行..."
|
placeholder="输入您的想法,按Enter发送,Shift+Enter换行,可粘贴图片..."
|
||||||
class="flex-1 auto-resize-input border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
|
class="flex-1 auto-resize-input border-2 border-gray-800 focus:border-cyan-500 focus:outline-none transition-colors font-medium"
|
||||||
rows="1"
|
rows="1"
|
||||||
></textarea>
|
></textarea>
|
||||||
@@ -76,6 +85,11 @@
|
|||||||
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
|
<iconify-icon icon="ph:paper-plane-tilt-fill" class="text-3xl"></iconify-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 图片功能提示 -->
|
||||||
|
<div id="image-vision-disabled-tip" class="mt-1 text-xs text-gray-400 hidden">
|
||||||
|
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||||
|
图片解析功能已禁用
|
||||||
|
</div>
|
||||||
<div id="chat-quick-actions" class="mt-2 flex flex-wrap gap-2 hidden"></div>
|
<div id="chat-quick-actions" class="mt-2 flex flex-wrap gap-2 hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,6 +195,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片解析开关 -->
|
||||||
|
<div class="flex items-center justify-between py-3 border-t-2 border-gray-200 mt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<iconify-icon icon="ph:eye-bold" class="text-lg text-cyan-600"></iconify-icon>
|
||||||
|
<div>
|
||||||
|
<label class="block font-bold text-gray-800">启用图片解析</label>
|
||||||
|
<span class="text-xs text-gray-500">允许上传图片并由AI进行视觉理解</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input type="checkbox" id="config-enable-vision" class="sr-only peer" checked>
|
||||||
|
<div class="w-11 h-6 bg-gray-300 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-500"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 状态显示 -->
|
<!-- 状态显示 -->
|
||||||
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
<div id="config-status" class="p-3 border-2 border-gray-300 bg-gray-50 text-sm text-gray-600 hidden">
|
||||||
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
<iconify-icon icon="ph:info" class="align-middle"></iconify-icon>
|
||||||
@@ -251,6 +280,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片预览模态窗 -->
|
||||||
|
<div id="image-preview-modal" class="modal-overlay">
|
||||||
|
<div class="image-preview-modal-content">
|
||||||
|
<button id="close-image-preview-btn" class="absolute top-4 right-4 text-white hover:text-gray-300 transition-colors z-10">
|
||||||
|
<iconify-icon icon="ph:x-bold" class="text-3xl"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
<img id="image-preview-full" src="" alt="图片预览" class="max-w-full max-h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 引入JavaScript文件 -->
|
<!-- 引入JavaScript文件 -->
|
||||||
<script src="js/utils.js"></script>
|
<script src="js/utils.js"></script>
|
||||||
<script src="js/services/storage-service.js"></script>
|
<script src="js/services/storage-service.js"></script>
|
||||||
|
|||||||
125
js/apiclient.js
125
js/apiclient.js
@@ -2,13 +2,23 @@
|
|||||||
* API客户端 - 处理与AI服务的交互
|
* API客户端 - 处理与AI服务的交互
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 图片上传配置常量
|
||||||
|
const IMAGE_CONFIG = {
|
||||||
|
maxCount: 4, // 最大图片数量
|
||||||
|
maxSizeBytes: 4 * 1024 * 1024, // 单张最大4MB
|
||||||
|
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
|
||||||
|
allowedExtensions: ['.jpg', '.jpeg', '.png', '.webp', '.gif']
|
||||||
|
};
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = {
|
this.config = {
|
||||||
url: '',
|
url: '',
|
||||||
key: '',
|
key: '',
|
||||||
model: ''
|
model: '',
|
||||||
|
enableVision: true // 默认启用图片解析
|
||||||
};
|
};
|
||||||
|
this.runtimeConfig = null; // 运行时配置(从config.json加载)
|
||||||
this.promptMap = {};
|
this.promptMap = {};
|
||||||
this.promptFiles = {
|
this.promptFiles = {
|
||||||
canvas: 'prompts/canvas-prompt.txt',
|
canvas: 'prompts/canvas-prompt.txt',
|
||||||
@@ -32,15 +42,72 @@ class APIClient {
|
|||||||
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
'你是一个可靠的智能助手,请直接回答用户的问题并提供结构化输出。'
|
||||||
};
|
};
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
|
this.loadRuntimeConfig(); // 加载运行时配置
|
||||||
this.preloadPrompts(Object.keys(this.promptFiles));
|
this.preloadPrompts(Object.keys(this.promptFiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载运行时配置(从config.json,支持Docker环境变量注入)
|
||||||
|
async loadRuntimeConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('config.json');
|
||||||
|
if (response.ok) {
|
||||||
|
this.runtimeConfig = await response.json();
|
||||||
|
// 运行时配置优先级高于本地存储
|
||||||
|
if (this.runtimeConfig.enableVision !== undefined) {
|
||||||
|
this.config.enableVision = this.runtimeConfig.enableVision;
|
||||||
|
}
|
||||||
|
console.log('运行时配置已加载:', this.runtimeConfig);
|
||||||
|
|
||||||
|
// 触发配置更新事件,通知UI同步
|
||||||
|
this.notifyConfigUpdated();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// config.json不存在时静默失败,使用默认配置
|
||||||
|
console.log('未找到运行时配置文件,使用默认配置');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发配置更新事件
|
||||||
|
notifyConfigUpdated() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent('vision-config-updated', {
|
||||||
|
detail: {
|
||||||
|
enableVision: this.isVisionEnabled(),
|
||||||
|
isRuntimeLocked: this.runtimeConfig && this.runtimeConfig.enableVision !== undefined
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查Vision配置是否被运行时锁定
|
||||||
|
isVisionConfigLocked() {
|
||||||
|
return this.runtimeConfig && this.runtimeConfig.enableVision !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// 加载API配置
|
// 加载API配置
|
||||||
loadConfig() {
|
loadConfig() {
|
||||||
const savedConfig = Utils.storage.get('apiConfig');
|
const savedConfig = Utils.storage.get('apiConfig');
|
||||||
if (savedConfig) {
|
if (savedConfig) {
|
||||||
this.config = { ...this.config, ...savedConfig };
|
this.config = { ...this.config, ...savedConfig };
|
||||||
}
|
}
|
||||||
|
// 确保enableVision有默认值
|
||||||
|
if (this.config.enableVision === undefined) {
|
||||||
|
this.config.enableVision = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片解析是否启用
|
||||||
|
isVisionEnabled() {
|
||||||
|
// 运行时配置优先级最高
|
||||||
|
if (this.runtimeConfig && this.runtimeConfig.enableVision !== undefined) {
|
||||||
|
return this.runtimeConfig.enableVision;
|
||||||
|
}
|
||||||
|
return this.config.enableVision !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片配置
|
||||||
|
getImageConfig() {
|
||||||
|
return { ...IMAGE_CONFIG };
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadPrompts(keys = []) {
|
preloadPrompts(keys = []) {
|
||||||
@@ -118,29 +185,71 @@ class APIClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildMessagesForModule(manifest, userMessage, contextMessages = []) {
|
async buildMessagesForModule(manifest, userMessage, contextMessages = [], images = []) {
|
||||||
const prompt =
|
const prompt =
|
||||||
(manifest && manifest.promptKey
|
(manifest && manifest.promptKey
|
||||||
? await this.ensurePrompt(manifest.promptKey)
|
? await this.ensurePrompt(manifest.promptKey)
|
||||||
: null) || this.promptFallbacks.default;
|
: null) || this.promptFallbacks.default;
|
||||||
|
|
||||||
|
// 构建用户消息内容(支持图片)
|
||||||
|
const userContent = this.buildUserContent(userMessage, images);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ role: 'system', content: prompt },
|
{ role: 'system', content: prompt },
|
||||||
...contextMessages,
|
...contextMessages,
|
||||||
{ role: 'user', content: userMessage }
|
{ role: 'user', content: userContent }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户消息内容(支持图片的OpenAI Vision API格式)
|
||||||
|
* @param {string} text - 文本内容
|
||||||
|
* @param {Array} images - 图片数组,每项包含 { base64, mimeType }
|
||||||
|
* @returns {string|Array} - 纯文本或多模态内容数组
|
||||||
|
*/
|
||||||
|
buildUserContent(text, images = []) {
|
||||||
|
// 如果没有图片或未启用Vision,返回纯文本
|
||||||
|
if (!images || images.length === 0 || !this.isVisionEnabled()) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建多模态内容数组(OpenAI Vision API格式)
|
||||||
|
const content = [];
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
|
for (const img of images) {
|
||||||
|
content.push({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: `data:${img.mimeType};base64,${img.base64}`,
|
||||||
|
detail: 'auto' // 可选: 'low', 'high', 'auto'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文本(放在图片后面)
|
||||||
|
if (text && text.trim()) {
|
||||||
|
content.push({
|
||||||
|
type: 'text',
|
||||||
|
text: text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
async generateModuleCompletion(
|
async generateModuleCompletion(
|
||||||
manifest,
|
manifest,
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages = [],
|
contextMessages = [],
|
||||||
options = {}
|
options = {},
|
||||||
|
images = []
|
||||||
) {
|
) {
|
||||||
const messages = await this.buildMessagesForModule(
|
const messages = await this.buildMessagesForModule(
|
||||||
manifest,
|
manifest,
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages
|
contextMessages,
|
||||||
|
images
|
||||||
);
|
);
|
||||||
return this.sendChatMessage(messages, options);
|
return this.sendChatMessage(messages, options);
|
||||||
}
|
}
|
||||||
@@ -151,12 +260,14 @@ class APIClient {
|
|||||||
contextMessages = [],
|
contextMessages = [],
|
||||||
onChunk,
|
onChunk,
|
||||||
onComplete,
|
onComplete,
|
||||||
options = {}
|
options = {},
|
||||||
|
images = []
|
||||||
) {
|
) {
|
||||||
const messages = await this.buildMessagesForModule(
|
const messages = await this.buildMessagesForModule(
|
||||||
manifest,
|
manifest,
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages
|
contextMessages,
|
||||||
|
images
|
||||||
);
|
);
|
||||||
return this.sendChatMessageStream(messages, options, onChunk, onComplete);
|
return this.sendChatMessageStream(messages, options, onChunk, onComplete);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
this.mermaidPanZoom = null;
|
this.mermaidPanZoom = null;
|
||||||
this.mermaidInitialized = false;
|
this.mermaidInitialized = false;
|
||||||
|
|
||||||
|
// 图片上传状态管理
|
||||||
|
this.pendingImages = []; // 待发送的图片列表 { id, file, blobUrl, base64?, mimeType }
|
||||||
|
|
||||||
this.globalStore = moduleRuntime.storageService.global();
|
this.globalStore = moduleRuntime.storageService.global();
|
||||||
this.activeModuleId = null;
|
this.activeModuleId = null;
|
||||||
|
|
||||||
@@ -57,6 +60,16 @@
|
|||||||
this.el.clearHistoryBtn = document.getElementById('clear-history-btn');
|
this.el.clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||||
this.el.chatHistory = document.getElementById('chat-history');
|
this.el.chatHistory = document.getElementById('chat-history');
|
||||||
|
|
||||||
|
// 图片上传相关
|
||||||
|
this.el.imageUploadBtn = document.getElementById('image-upload-btn');
|
||||||
|
this.el.imageFileInput = document.getElementById('image-file-input');
|
||||||
|
this.el.imagePreviewContainer = document.getElementById('image-preview-container');
|
||||||
|
this.el.imagePreviewModal = document.getElementById('image-preview-modal');
|
||||||
|
this.el.imagePreviewFull = document.getElementById('image-preview-full');
|
||||||
|
this.el.closeImagePreviewBtn = document.getElementById('close-image-preview-btn');
|
||||||
|
this.el.imageVisionDisabledTip = document.getElementById('image-vision-disabled-tip');
|
||||||
|
this.el.configEnableVision = document.getElementById('config-enable-vision');
|
||||||
|
|
||||||
// 视图区域
|
// 视图区域
|
||||||
this.el.viewer = document.getElementById('svg-viewer');
|
this.el.viewer = document.getElementById('svg-viewer');
|
||||||
this.el.placeholderText =
|
this.el.placeholderText =
|
||||||
@@ -287,6 +300,101 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图片上传相关事件绑定
|
||||||
|
this.bindImageUploadEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定图片上传相关事件
|
||||||
|
*/
|
||||||
|
bindImageUploadEvents() {
|
||||||
|
// 图片上传按钮点击
|
||||||
|
if (this.el.imageUploadBtn && this.el.imageFileInput) {
|
||||||
|
this.el.imageUploadBtn.addEventListener('click', () => {
|
||||||
|
if (!this.apiClient.isVisionEnabled()) {
|
||||||
|
this.showImageError('图片解析功能已禁用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.el.imageFileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件选择变化
|
||||||
|
if (this.el.imageFileInput) {
|
||||||
|
this.el.imageFileInput.addEventListener('change', (event) => {
|
||||||
|
const files = Array.from(event.target.files || []);
|
||||||
|
this.handleImageFiles(files);
|
||||||
|
event.target.value = ''; // 清空以允许重复选择同一文件
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粘贴事件(支持图片粘贴)
|
||||||
|
if (this.el.chatInput) {
|
||||||
|
this.el.chatInput.addEventListener('paste', (event) => {
|
||||||
|
if (!this.apiClient.isVisionEnabled()) return;
|
||||||
|
|
||||||
|
const items = event.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
const imageFiles = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.startsWith('image/')) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) imageFiles.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageFiles.length > 0) {
|
||||||
|
// 不阻止默认行为,允许同时粘贴文本
|
||||||
|
this.handleImageFiles(imageFiles);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览容器点击事件(删除和预览)
|
||||||
|
if (this.el.imagePreviewContainer) {
|
||||||
|
this.el.imagePreviewContainer.addEventListener('click', (event) => {
|
||||||
|
const deleteBtn = event.target.closest('.image-thumbnail-delete');
|
||||||
|
if (deleteBtn) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const imageId = deleteBtn.dataset.imageId;
|
||||||
|
this.removeImage(imageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnailItem = event.target.closest('.image-thumbnail-item');
|
||||||
|
if (thumbnailItem) {
|
||||||
|
const imageId = thumbnailItem.dataset.imageId;
|
||||||
|
this.openImagePreview(imageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览模态窗关闭
|
||||||
|
if (this.el.closeImagePreviewBtn) {
|
||||||
|
this.el.closeImagePreviewBtn.addEventListener('click', () => {
|
||||||
|
this.closeImagePreviewModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.el.imagePreviewModal) {
|
||||||
|
this.el.imagePreviewModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === this.el.imagePreviewModal) {
|
||||||
|
this.closeImagePreviewModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化图片上传UI状态
|
||||||
|
this.updateImageUploadUI();
|
||||||
|
|
||||||
|
// 监听运行时配置更新事件
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('vision-config-updated', (event) => {
|
||||||
|
this.updateImageUploadUI();
|
||||||
|
this.updateVisionConfigUI(event.detail);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupModuleSwitcher() {
|
setupModuleSwitcher() {
|
||||||
@@ -461,14 +569,21 @@
|
|||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
const actionsHtml = this.buildMessageActions(message, options);
|
const actionsHtml = this.buildMessageActions(message, options);
|
||||||
|
|
||||||
|
// 构建图片HTML(用于用户消息)
|
||||||
|
const imagesHtml = this.buildMessageImagesHtml(message.images);
|
||||||
|
|
||||||
if (message.type === 'user') {
|
if (message.type === 'user') {
|
||||||
wrapper.className = 'flex justify-end';
|
wrapper.className = 'flex justify-end';
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
<div class="chat-bubble-user message-with-delete" data-message-id="${message.id}">
|
||||||
<div>${Utils.escapeHtml(message.content)}</div>
|
${imagesHtml}
|
||||||
|
<div>${Utils.escapeHtml(message.content || '')}</div>
|
||||||
${actionsHtml}
|
${actionsHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// 绑定图片点击预览事件
|
||||||
|
this.bindBubbleImagePreview(wrapper, message.images);
|
||||||
} else if (message.type === 'error') {
|
} else if (message.type === 'error') {
|
||||||
wrapper.className = 'flex justify-start';
|
wrapper.className = 'flex justify-start';
|
||||||
wrapper.innerHTML = `
|
wrapper.innerHTML = `
|
||||||
@@ -509,6 +624,47 @@
|
|||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建消息中图片的HTML
|
||||||
|
* @param {Array} images - 图片数组
|
||||||
|
* @returns {string} - HTML字符串
|
||||||
|
*/
|
||||||
|
buildMessageImagesHtml(images) {
|
||||||
|
if (!images || images.length === 0) return '';
|
||||||
|
|
||||||
|
const imageItems = images.map((img, index) => {
|
||||||
|
const src = `data:${img.mimeType};base64,${img.base64}`;
|
||||||
|
return `
|
||||||
|
<div class="chat-bubble-image" data-image-index="${index}">
|
||||||
|
<img src="${src}" alt="图片 ${index + 1}" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `<div class="chat-bubble-images">${imageItems}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定气泡中图片的点击预览事件
|
||||||
|
* @param {HTMLElement} wrapper - 消息包装元素
|
||||||
|
* @param {Array} images - 图片数组
|
||||||
|
*/
|
||||||
|
bindBubbleImagePreview(wrapper, images) {
|
||||||
|
if (!images || images.length === 0) return;
|
||||||
|
|
||||||
|
const imageElements = wrapper.querySelectorAll('.chat-bubble-image');
|
||||||
|
imageElements.forEach((el, index) => {
|
||||||
|
el.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const img = images[index];
|
||||||
|
if (img && this.el.imagePreviewModal && this.el.imagePreviewFull) {
|
||||||
|
this.el.imagePreviewFull.src = `data:${img.mimeType};base64,${img.base64}`;
|
||||||
|
this.el.imagePreviewModal.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
buildMessageActions(message, options = {}) {
|
buildMessageActions(message, options = {}) {
|
||||||
const {
|
const {
|
||||||
allowRollback = false,
|
allowRollback = false,
|
||||||
@@ -692,7 +848,8 @@
|
|||||||
.map((msg) => ({
|
.map((msg) => ({
|
||||||
role: msg.type === 'user' ? 'user' : 'assistant',
|
role: msg.type === 'user' ? 'user' : 'assistant',
|
||||||
// 使用原始内容作为 LLM 上下文,兼容无 rawContent 的旧记录
|
// 使用原始内容作为 LLM 上下文,兼容无 rawContent 的旧记录
|
||||||
content: msg.rawContent || msg.content || ''
|
content: msg.rawContent || msg.content || '',
|
||||||
|
images: msg.type === 'user' ? msg.images || [] : undefined
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.isProcessing = true;
|
this.isProcessing = true;
|
||||||
@@ -702,7 +859,8 @@
|
|||||||
|
|
||||||
this.beginStreaming(manifest, {
|
this.beginStreaming(manifest, {
|
||||||
userMessage,
|
userMessage,
|
||||||
contextMessages
|
contextMessages,
|
||||||
|
images: userMessage.images || [] // 传递原始用户消息的图片
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,10 +903,14 @@
|
|||||||
return nextId;
|
return nextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage() {
|
async sendMessage() {
|
||||||
if (!this.el.chatInput) return;
|
if (!this.el.chatInput) return;
|
||||||
const message = this.el.chatInput.value.trim();
|
const message = this.el.chatInput.value.trim();
|
||||||
if (!message || this.isProcessing) return;
|
const hasImages = this.pendingImages.length > 0;
|
||||||
|
|
||||||
|
// 必须有文本或图片才能发送
|
||||||
|
if (!message && !hasImages) return;
|
||||||
|
if (this.isProcessing) return;
|
||||||
|
|
||||||
if (!this.apiClient.isConfigValid()) {
|
if (!this.apiClient.isConfigValid()) {
|
||||||
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
|
alert('⚠️ 请先配置API设置!点击右上角齿轮图标进行配置。');
|
||||||
@@ -756,12 +918,25 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 准备图片数据(转换为base64)
|
||||||
|
let images = [];
|
||||||
|
if (hasImages && this.apiClient.isVisionEnabled()) {
|
||||||
|
try {
|
||||||
|
images = await this.prepareImagesForSend();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片处理失败:', error);
|
||||||
|
this.showImageError('图片处理失败,请重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const manifest = this.getActiveManifest();
|
const manifest = this.getActiveManifest();
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
id: Utils.generateId('msg'),
|
id: Utils.generateId('msg'),
|
||||||
type: 'user',
|
type: 'user',
|
||||||
content: message,
|
content: message,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
images: images.length > 0 ? images : undefined // 保存图片数据到消息
|
||||||
};
|
};
|
||||||
|
|
||||||
this.conversationService.appendMessage(manifest, userMessage);
|
this.conversationService.appendMessage(manifest, userMessage);
|
||||||
@@ -769,6 +944,9 @@
|
|||||||
this.el.chatInput.value = '';
|
this.el.chatInput.value = '';
|
||||||
Utils.autoResizeTextarea(this.el.chatInput);
|
Utils.autoResizeTextarea(this.el.chatInput);
|
||||||
|
|
||||||
|
// 清空待发送图片
|
||||||
|
this.clearPendingImages();
|
||||||
|
|
||||||
const context = this.conversationService.buildContext(manifest);
|
const context = this.conversationService.buildContext(manifest);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
console.warn('无法构建上下文,终止发送');
|
console.warn('无法构建上下文,终止发送');
|
||||||
@@ -782,7 +960,8 @@
|
|||||||
|
|
||||||
this.beginStreaming(manifest, {
|
this.beginStreaming(manifest, {
|
||||||
userMessage: context.userMessage,
|
userMessage: context.userMessage,
|
||||||
contextMessages: context.contextMessages
|
contextMessages: context.contextMessages,
|
||||||
|
images: images // 传递图片数据
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,7 +1037,8 @@
|
|||||||
payload.contextMessages,
|
payload.contextMessages,
|
||||||
handleChunk,
|
handleChunk,
|
||||||
handleComplete,
|
handleComplete,
|
||||||
STREAM_DEFAULT_OPTIONS
|
STREAM_DEFAULT_OPTIONS,
|
||||||
|
payload.images || [] // 传递图片数据
|
||||||
)
|
)
|
||||||
.then((streamHandle) => {
|
.then((streamHandle) => {
|
||||||
this.activeStreamHandle = streamHandle;
|
this.activeStreamHandle = streamHandle;
|
||||||
@@ -2705,6 +2885,11 @@
|
|||||||
if (this.el.apiUrlInput) this.el.apiUrlInput.value = config.url || '';
|
if (this.el.apiUrlInput) this.el.apiUrlInput.value = config.url || '';
|
||||||
if (this.el.apiKeyInput) this.el.apiKeyInput.value = config.key || '';
|
if (this.el.apiKeyInput) this.el.apiKeyInput.value = config.key || '';
|
||||||
if (this.el.apiModelInput) this.el.apiModelInput.value = config.model || '';
|
if (this.el.apiModelInput) this.el.apiModelInput.value = config.model || '';
|
||||||
|
if (this.el.configEnableVision) {
|
||||||
|
this.el.configEnableVision.checked = config.enableVision !== false;
|
||||||
|
}
|
||||||
|
// 更新图片上传UI状态
|
||||||
|
this.updateImageUploadUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
async testAPI() {
|
async testAPI() {
|
||||||
@@ -2721,7 +2906,8 @@
|
|||||||
const config = {
|
const config = {
|
||||||
url: this.el.apiUrlInput?.value.trim(),
|
url: this.el.apiUrlInput?.value.trim(),
|
||||||
key: this.el.apiKeyInput?.value.trim(),
|
key: this.el.apiKeyInput?.value.trim(),
|
||||||
model: this.el.apiModelInput?.value.trim()
|
model: this.el.apiModelInput?.value.trim(),
|
||||||
|
enableVision: this.el.configEnableVision?.checked !== false
|
||||||
};
|
};
|
||||||
if (!config.url || !config.key || !config.model) {
|
if (!config.url || !config.key || !config.model) {
|
||||||
this.setConfigStatus('error', '请填写完整的配置');
|
this.setConfigStatus('error', '请填写完整的配置');
|
||||||
@@ -2729,6 +2915,8 @@
|
|||||||
}
|
}
|
||||||
this.apiClient.saveConfig(config);
|
this.apiClient.saveConfig(config);
|
||||||
this.setConfigStatus('success', '配置已保存');
|
this.setConfigStatus('success', '配置已保存');
|
||||||
|
// 更新图片上传UI状态
|
||||||
|
this.updateImageUploadUI();
|
||||||
//setTimeout(() => this.closeConfigModal(), 600);
|
//setTimeout(() => this.closeConfigModal(), 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2776,6 +2964,255 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 图片上传相关方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理图片文件(上传或粘贴)
|
||||||
|
* @param {File[]} files - 文件列表
|
||||||
|
*/
|
||||||
|
handleImageFiles(files) {
|
||||||
|
const config = this.apiClient.getImageConfig();
|
||||||
|
const currentCount = this.pendingImages.length;
|
||||||
|
const availableSlots = config.maxCount - currentCount;
|
||||||
|
|
||||||
|
if (availableSlots <= 0) {
|
||||||
|
this.showImageError(`最多只能上传 ${config.maxCount} 张图片`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filesToAdd = files.slice(0, availableSlots);
|
||||||
|
|
||||||
|
for (const file of filesToAdd) {
|
||||||
|
// 验证文件类型
|
||||||
|
if (!config.allowedTypes.includes(file.type)) {
|
||||||
|
this.showImageError(`不支持的图片格式: ${file.type}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
if (file.size > config.maxSizeBytes) {
|
||||||
|
const maxSizeMB = config.maxSizeBytes / (1024 * 1024);
|
||||||
|
this.showImageError(`图片大小超过限制 (最大 ${maxSizeMB}MB)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建图片对象
|
||||||
|
const imageObj = {
|
||||||
|
id: Utils.generateId('img'),
|
||||||
|
file: file,
|
||||||
|
blobUrl: URL.createObjectURL(file),
|
||||||
|
mimeType: file.type,
|
||||||
|
base64: null // 发送时再转换
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pendingImages.push(imageObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderImageThumbnails();
|
||||||
|
|
||||||
|
if (files.length > availableSlots) {
|
||||||
|
this.showImageError(`已达到最大图片数量限制 (${config.maxCount} 张)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染图片缩略图
|
||||||
|
*/
|
||||||
|
renderImageThumbnails() {
|
||||||
|
if (!this.el.imagePreviewContainer) return;
|
||||||
|
|
||||||
|
if (this.pendingImages.length === 0) {
|
||||||
|
this.el.imagePreviewContainer.classList.add('hidden');
|
||||||
|
this.el.imagePreviewContainer.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.el.imagePreviewContainer.classList.remove('hidden');
|
||||||
|
this.el.imagePreviewContainer.innerHTML = this.pendingImages.map((img, index) => `
|
||||||
|
<div class="image-thumbnail-item" data-image-id="${img.id}" title="点击预览">
|
||||||
|
<img src="${img.blobUrl}" alt="图片 ${index + 1}" />
|
||||||
|
<button class="image-thumbnail-delete" data-image-id="${img.id}" title="删除">
|
||||||
|
<iconify-icon icon="ph:x-bold"></iconify-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定图片
|
||||||
|
* @param {string} imageId - 图片ID
|
||||||
|
*/
|
||||||
|
removeImage(imageId) {
|
||||||
|
const index = this.pendingImages.findIndex(img => img.id === imageId);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const img = this.pendingImages[index];
|
||||||
|
// 释放Blob URL
|
||||||
|
if (img.blobUrl) {
|
||||||
|
URL.revokeObjectURL(img.blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingImages.splice(index, 1);
|
||||||
|
this.renderImageThumbnails();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有待发送图片
|
||||||
|
*/
|
||||||
|
clearPendingImages() {
|
||||||
|
for (const img of this.pendingImages) {
|
||||||
|
if (img.blobUrl) {
|
||||||
|
URL.revokeObjectURL(img.blobUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.pendingImages = [];
|
||||||
|
this.renderImageThumbnails();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图片预览模态窗
|
||||||
|
* @param {string} imageId - 图片ID
|
||||||
|
*/
|
||||||
|
openImagePreview(imageId) {
|
||||||
|
const img = this.pendingImages.find(i => i.id === imageId);
|
||||||
|
if (!img || !this.el.imagePreviewModal || !this.el.imagePreviewFull) return;
|
||||||
|
|
||||||
|
this.el.imagePreviewFull.src = img.blobUrl;
|
||||||
|
this.el.imagePreviewModal.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭图片预览模态窗
|
||||||
|
*/
|
||||||
|
closeImagePreviewModal() {
|
||||||
|
if (!this.el.imagePreviewModal) return;
|
||||||
|
this.el.imagePreviewModal.classList.remove('active');
|
||||||
|
if (this.el.imagePreviewFull) {
|
||||||
|
this.el.imagePreviewFull.src = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示图片错误提示
|
||||||
|
* @param {string} message - 错误信息
|
||||||
|
*/
|
||||||
|
showImageError(message) {
|
||||||
|
// 创建toast提示
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'image-error-toast';
|
||||||
|
toast.textContent = message;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// 3秒后自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新图片上传UI状态(根据enableVision配置)
|
||||||
|
*/
|
||||||
|
updateImageUploadUI() {
|
||||||
|
const visionEnabled = this.apiClient.isVisionEnabled();
|
||||||
|
|
||||||
|
// 更新上传按钮状态
|
||||||
|
if (this.el.imageUploadBtn) {
|
||||||
|
this.el.imageUploadBtn.disabled = !visionEnabled;
|
||||||
|
this.el.imageUploadBtn.title = visionEnabled
|
||||||
|
? '上传图片 (支持粘贴)'
|
||||||
|
: '图片解析功能已禁用';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新禁用提示
|
||||||
|
if (this.el.imageVisionDisabledTip) {
|
||||||
|
this.el.imageVisionDisabledTip.classList.toggle('hidden', visionEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果禁用了Vision,清空待发送图片
|
||||||
|
if (!visionEnabled && this.pendingImages.length > 0) {
|
||||||
|
this.clearPendingImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新Vision配置UI(处理运行时锁定状态)
|
||||||
|
* @param {Object} detail - 配置详情 { enableVision, isRuntimeLocked }
|
||||||
|
*/
|
||||||
|
updateVisionConfigUI(detail) {
|
||||||
|
if (!this.el.configEnableVision) return;
|
||||||
|
|
||||||
|
const { enableVision, isRuntimeLocked } = detail;
|
||||||
|
|
||||||
|
// 更新复选框状态
|
||||||
|
this.el.configEnableVision.checked = enableVision;
|
||||||
|
|
||||||
|
// 如果被运行时配置锁定,禁用复选框并添加提示
|
||||||
|
if (isRuntimeLocked) {
|
||||||
|
this.el.configEnableVision.disabled = true;
|
||||||
|
|
||||||
|
// 添加锁定提示
|
||||||
|
const parent = this.el.configEnableVision.closest('.flex');
|
||||||
|
if (parent) {
|
||||||
|
let lockHint = parent.querySelector('.vision-lock-hint');
|
||||||
|
if (!lockHint) {
|
||||||
|
lockHint = document.createElement('span');
|
||||||
|
lockHint.className = 'vision-lock-hint text-xs text-orange-600 ml-2';
|
||||||
|
lockHint.innerHTML = '<iconify-icon icon="ph:lock-bold" class="align-middle"></iconify-icon> 由部署配置锁定';
|
||||||
|
parent.appendChild(lockHint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.el.configEnableVision.disabled = false;
|
||||||
|
|
||||||
|
// 移除锁定提示
|
||||||
|
const parent = this.el.configEnableVision.closest('.flex');
|
||||||
|
if (parent) {
|
||||||
|
const lockHint = parent.querySelector('.vision-lock-hint');
|
||||||
|
if (lockHint) {
|
||||||
|
lockHint.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将File转换为Base64
|
||||||
|
* @param {File} file - 文件对象
|
||||||
|
* @returns {Promise<string>} - Base64字符串(不含data:前缀)
|
||||||
|
*/
|
||||||
|
fileToBase64(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
// 移除 "data:image/xxx;base64," 前缀
|
||||||
|
const base64 = reader.result.split(',')[1];
|
||||||
|
resolve(base64);
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 准备图片数据用于API发送(并行转换优化)
|
||||||
|
* @returns {Promise<Array>} - 图片数据数组 [{ base64, mimeType }]
|
||||||
|
*/
|
||||||
|
async prepareImagesForSend() {
|
||||||
|
// 并行转换所有图片为base64
|
||||||
|
const conversionPromises = this.pendingImages.map(async (img) => {
|
||||||
|
// 如果还没有转换为base64,现在转换
|
||||||
|
if (!img.base64) {
|
||||||
|
img.base64 = await this.fileToBase64(img.file);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
base64: img.base64,
|
||||||
|
mimeType: img.mimeType
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(conversionPromises);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
global.AppShell = AppShell;
|
global.AppShell = AppShell;
|
||||||
|
|||||||
Reference in New Issue
Block a user