Files
metona-ollama-desktop/index.html
T
Metona 12843f09a5 feat: 智能滚动 - 流式回复时可向上查看历史,浮动按钮回到底部
- 流式输出时不再强制锁定底部,用户可自由滚动查看历史
- 滚动离开底部时显示「回到底部」浮动按钮(sticky 定位)
- 点击按钮平滑滚动并恢复自动跟踪
- 用户手动滚回底部时自动恢复跟踪
- 发送消息/新建会话时自动重置为跟踪状态
- 首次渲染(加载历史)强制滚到底部
2026-04-04 21:41:26 +08:00

241 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#0a0a1a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="description" content="Metona Ollama Client - 流式聊天、多模态、历史管理">
<link rel="manifest" href="manifest.json">
<link rel="icon" href="assets/icons/llama.ico" type="image/x-icon">
<title>Metona Ollama</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="app">
<!-- ═══════════════ 顶部导航 ═══════════════ -->
<header class="app-header">
<div class="header-left">
<span class="logo">🦙</span>
<span class="app-title">Metona Ollama</span>
<span class="app-version">v2.0.0</span>
</div>
<div class="header-right">
<div class="conn-status pending" id="connStatus" title="连接状态">
<span class="status-dot"></span>
<span class="status-label">未连接</span>
</div>
<button class="icon-btn" id="btnNewChat" title="新建会话">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
</button>
<button class="icon-btn" id="btnHistory" title="历史记录">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>
</svg>
</button>
<button class="icon-btn" id="btnSettings" title="设置">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
</button>
</div>
</header>
<!-- ═══════════════ 模型选择栏 ═══════════════ -->
<div class="model-bar">
<div class="model-bar-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="4" y="4" width="16" height="16" rx="2"/>
<path d="M9 9h6M9 12h6M9 15h4"/>
</svg>
</div>
<div class="model-select-wrap">
<select class="model-select" id="modelSelect">
<option value="">正在加载模型...</option>
</select>
<span class="model-select-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</span>
</div>
<div class="model-badges" id="modelBadges">
<span class="model-badge think-badge" id="badgeThink" style="display:none;">🧠 Think</span>
<span class="model-badge vision-badge" id="badgeVision" style="display:none;">👁️ Vision</span>
</div>
</div>
<!-- ═══════════════ 聊天区域 ═══════════════ -->
<main class="chat-area" id="chatArea">
<div class="empty-state" id="emptyState">
<div class="empty-icon">
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="80" stroke="rgba(0,245,212,0.2)" stroke-width="2"/>
<circle cx="100" cy="100" r="60" stroke="rgba(123,47,247,0.2)" stroke-width="2"/>
<path d="M70 90c0-16.57 13.43-30 30-30s30 13.43 30 30" stroke="rgba(0,245,212,0.5)" stroke-width="2" stroke-linecap="round"/>
<circle cx="85" cy="100" r="5" fill="rgba(0,245,212,0.6)"/>
<circle cx="115" cy="100" r="5" fill="rgba(0,245,212,0.6)"/>
<path d="M88 120c4 4 20 4 24 0" stroke="rgba(0,245,212,0.4)" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<h2>开始对话</h2>
<p>选择一个模型,输入消息开始聊天</p>
</div>
<div class="messages" id="messagesContainer" style="display:none;"></div>
<button class="scroll-to-bottom" id="scrollToBottom" style="display:none;" title="回到最新">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span>回到底部</span>
</button>
</main>
<!-- ═══════════════ 历史只读提示栏 ═══════════════ -->
<div class="history-view-bar" id="historyBar" style="display:none;">
<span>📖 正在查看历史记录(只读模式)</span>
<button class="btn btn-sm btn-outline" id="btnExitHistory">返回新会话</button>
</div>
<!-- ═══════════════ 输入区域 ═══════════════ -->
<div class="input-area" id="inputArea">
<div class="image-preview" id="imagePreview" style="display:none;"></div>
<div class="file-preview" id="filePreview" style="display:none;"></div>
<div class="input-row">
<button class="icon-btn attach-btn" id="btnAttachImg" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</button>
<button class="icon-btn attach-btn" id="btnAttachFile" title="上传文件(文本/代码)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
</button>
<input type="file" id="fileInput" accept="image/*" multiple style="display:none;">
<input type="file" id="textFileInput" accept=".txt,.md,.log,.csv,.py,.pyw,.js,.mjs,.cjs,.ts,.tsx,.jsx,.java,.c,.h,.cpp,.cc,.hpp,.go,.rs,.rb,.php,.sh,.bash,.zsh,.sql,.html,.htm,.css,.json,.jsonl,.xml,.svg,.yaml,.yml,.toml,.ini,.cfg,.conf,.swift,.kt,.kts,.scala,.lua,.r,.m,.cs,.fs,.ex,.exs,.erl,.hs,.clj,.lisp,.diff,.patch,Dockerfile,Makefile,.rst,.tsv,.vb,.pl,.mm,.pyi,.pyw,.fsharp,.makefile,.dockerfile" multiple style="display:none;">
<textarea class="chat-input" id="chatInput" placeholder="输入消息..." rows="1"></textarea>
<label class="think-toggle" id="thinkToggleWrap" title="Think 模式未可用">
<input type="checkbox" id="toggleThink" disabled>
<span class="think-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2a7 7 0 0 1 7 7c0 2.38-1.19 4.47-3 5.74V17a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1v-2.26C6.19 13.47 5 11.38 5 9a7 7 0 0 1 7-7z"/>
<line x1="9" y1="21" x2="15" y2="21"/>
</svg>
</span>
</label>
<button class="send-btn" id="btnSend">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
</div>
</div>
</div>
<!-- ═══════════════ 设置模态框 ═══════════════ -->
<div class="modal-overlay" id="settingsModal" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3>⚙️ 设置</h3>
<button class="icon-btn" id="btnCloseSettings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="setting-group">
<label class="setting-label">Ollama 服务地址</label>
<input class="setting-input" id="inputServerUrl" type="url" placeholder="http://127.0.0.1:11434">
<div class="connection-info" id="connInfo">
<span class="status-badge" id="connBadge">检测中...</span>
<span class="server-version" id="serverVersion"></span>
</div>
<div class="cors-hint" id="corsHint" style="display:none;">
<p>⚠️ 连接失败,可能是 CORS 问题。请在启动 Ollama 时设置环境变量:</p>
<code>OLLAMA_ORIGINS="*" ollama serve</code>
</div>
</div>
<div class="setting-group">
<label class="setting-label">
系统提示词(System Prompt
<label class="toggle-label" style="margin-left:auto;display:inline-flex;">
<input type="checkbox" id="toggleSystemPrompt">
<span class="toggle-slider"></span>
</label>
</label>
<textarea class="setting-input" id="inputSystemPrompt" rows="4" placeholder="例如:你是一个专业的编程助手,用中文回答。" style="resize:vertical;min-height:80px;" disabled></textarea>
<label class="setting-label" style="margin-top:12px;">上下文长度(num_ctx</label>
<div style="display:flex;gap:8px;align-items:center;">
<input class="setting-input" id="inputNumCtx" type="number" min="512" max="131072" step="512" value="24576" style="margin-bottom:0;">
<span class="text-muted" style="white-space:nowrap;">tokens</span>
</div>
<p class="text-muted" style="margin-top:4px;font-size:11px;">常见值:2048 / 4096 / 8192 / 32768。值越大上下文越长,显存占用越高。</p>
</div>
<div class="setting-group">
<label class="setting-label">运行中的模型</label>
<div class="running-models" id="runningModels">
<p class="text-muted">无运行中的模型</p>
</div>
</div>
<div class="setting-group">
<label class="setting-label">显存管理</label>
<button class="btn btn-danger" id="btnReleaseVRAM">释放显存(卸载模型)</button>
</div>
<div class="setting-group">
<label class="setting-label">数据管理</label>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn btn-outline" id="btnExportAll">📦 导出全部会话</button>
<button class="btn btn-outline" id="btnImportSessions">📥 导入会话</button>
<input type="file" id="importFileInput" accept=".json" style="display:none;">
<button class="btn btn-danger" id="btnClearAllHistory">清空所有历史</button>
</div>
<p class="text-muted" style="margin-top:6px;font-size:11px;">导出格式为 JSON,可备份后在其他设备导入恢复。</p>
</div>
</div>
</div>
</div>
<!-- ═══════════════ 历史记录模态框 ═══════════════ -->
<div class="modal-overlay" id="historyModal" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3>🕐 历史记录</h3>
<button class="icon-btn" id="btnCloseHistory">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body">
<div class="history-search-wrap">
<input class="history-search-input" id="historySearchInput" type="text" placeholder="搜索会话标题或内容...">
<button class="history-search-clear" id="historySearchClear" style="display:none;"></button>
</div>
<div class="history-list" id="historyList"></div>
<div class="history-pagination" id="historyPagination"></div>
</div>
</div>
</div>
<!-- ═══════════════ 图片预览 Lightbox ═══════════════ -->
<div class="lightbox-overlay" id="lightbox" style="display:none;">
<button class="lightbox-close" id="lightboxClose"></button>
<img class="lightbox-img" id="lightboxImg" src="" alt="预览">
</div>
<!-- ═══════════════ Toast 通知容器 ═══════════════ -->
<div class="toast-container" id="toastContainer"></div>
<!-- ═══════════════ 应用入口(ES Module)═══════════════ -->
<script type="module" src="./js/app.js"></script>
</body>
</html>