feat: improve onboarding and copy ux

This commit is contained in:
JOJO 2026-01-19 21:02:24 +08:00
parent 026588bba3
commit 88dc7e02a4
6 changed files with 211 additions and 2 deletions

View File

@ -4,7 +4,7 @@ import os
import shlex import shlex
_DEFAULT_ALLOWED_EXTENSIONS = ( _DEFAULT_ALLOWED_EXTENSIONS = (
".txt,.md,.rst,.py,.js,.ts,.json,.yml,.yaml,.ini,.cfg,.conf," ".txt,.md,.rst,.py,.js,.ts,.json,.yml,.yaml,.ini,.cfg,.conf,.ipynb"
".csv,.tsv,.log,.mdx,.env,.sh,.bat,.ps1,.sql,.html,.css," ".csv,.tsv,.log,.mdx,.env,.sh,.bat,.ps1,.sql,.html,.css,"
".svg,.png,.jpg,.jpeg,.gif,.bmp,.webp,.ico,.pdf,.pptx,.docx," ".svg,.png,.jpg,.jpeg,.gif,.bmp,.webp,.ico,.pdf,.pptx,.docx,"
".xlsx,.mp3,.wav,.flac,.mp4,.mov,.mkv,.avi,.webm," ".xlsx,.mp3,.wav,.flac,.mp4,.mov,.mkv,.avi,.webm,"

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path fill="none" d="M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2M6.453 15h11.094M8.5 2h7"/></svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0zM22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/></svg>

After

Width:  |  Height:  |  Size: 381 B

View File

@ -12,12 +12,18 @@
<div id="app"></div> <div id="app"></div>
<script> <script>
// 全局复制代码块函数 // 全局复制代码块函数
function decodeHtmlEntities(text) {
const textarea = document.createElement('textarea');
textarea.innerHTML = text || '';
return textarea.value;
}
function copyCodeBlock(blockId) { function copyCodeBlock(blockId) {
const codeElement = document.querySelector(`[data-code-id="${blockId}"]`); const codeElement = document.querySelector(`[data-code-id="${blockId}"]`);
if (!codeElement) return; if (!codeElement) return;
const button = document.querySelector(`[data-code="${blockId}"]`); const button = document.querySelector(`[data-code="${blockId}"]`);
if (button && button.classList.contains('copied')) return; if (button && button.classList.contains('copied')) return;
const codeContent = codeElement?.dataset?.originalCode || codeElement?.textContent || ''; const raw = codeElement?.dataset?.originalCode || codeElement?.textContent || '';
const codeContent = decodeHtmlEntities(raw);
if (!button) { if (!button) {
navigator.clipboard.writeText(codeContent).catch((err) => console.error('复制失败:', err)); navigator.clipboard.writeText(codeContent).catch((err) => console.error('复制失败:', err));
return; return;

View File

@ -0,0 +1,195 @@
<template>
<transition name="overlay-fade">
<div v-if="open" class="exp-overlay">
<div class="exp-card">
<div class="exp-header">
<div>
<div class="title">第一次使用选择适合您的模式</div>
<div class="subtitle">速度联网与工具范围各不相同随时可在个人空间切换</div>
</div>
<button class="confirm-btn" type="button" @click="confirmSelection">确认并开始</button>
</div>
<div class="grid">
<div
v-for="item in modes"
:key="item.id"
class="mode-card"
:class="{ active: selected === item.id }"
@click="selected = item.id"
>
<div class="badge">{{ item.badge }}</div>
<div class="icon" v-html="item.icon" aria-hidden="true"></div>
<h3>{{ item.title }}</h3>
<p>{{ item.desc }}</p>
</div>
</div>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
type Mode = 'fast' | 'thinking' | 'research' | 'expert';
const props = defineProps<{
open: boolean;
initial?: Mode;
}>();
const emit = defineEmits<{
(event: 'close'): void;
(event: 'confirm', mode: Mode): void;
}>();
const selected = ref<Mode>(props.initial || 'fast');
watch(
() => props.initial,
(val) => {
if (val) selected.value = val;
}
);
const modes = [
{
id: 'fast',
badge: '极速',
title: '快速回答',
desc: '快速回答您的问题,会上网搜索,不会写文件。',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M13 2 5 14h6l-2 8 10-12h-6z"/></svg>`
},
{
id: 'thinking',
badge: '思考',
title: '思考后再答',
desc: '经过思考后完成任务,会上网搜索,不会写文件,回复时间较长。',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M12 18V5"/><path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"/><path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"/><path d="M17.997 5.125a4 4 0 0 1 2.526 5.77"/><path d="M18 18a4 4 0 0 0 2-7.464"/><path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"/><path d="M6 18a4 4 0 0 1-2-7.464"/><path d="M6.003 5.125a4 4 0 0 0-2.526 5.77"/></svg>`
},
{
id: 'research',
badge: '研究',
title: '多步骤研究',
desc: '多步骤思考与 20+ 工具完成复杂任务,回复时间可能很长。',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M9 3v5l-4 9a2 2 0 0 0 1.8 3h10.4A2 2 0 0 0 19 17l-4-9V3"/><path d="M9 8h6"/></svg>`
},
{
id: 'expert',
badge: '专家',
title: '全功能探索',
desc: '自由探索所有配置选项,适合智能体专家与从业者。',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M3 9 12 5l9 4-9 4-9-4Z"/><path d="M6 10v5c0 .6.4 1.2 1 1.4l5 2.1 5-2.1c.6-.2 1-.8 1-1.4v-5"/><path d="M12 13v6"/></svg>`
}
] as const;
function confirmSelection() {
emit('confirm', selected.value);
emit('close');
}
</script>
<style scoped>
.exp-overlay {
position: fixed;
inset: 0;
background: rgba(33, 24, 14, 0.55);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
z-index: 1200;
}
.exp-card {
width: min(1100px, 96vw);
background: rgba(255, 255, 255, 0.96);
border-radius: 18px;
padding: 24px 28px;
box-shadow: 0 20px 50px rgba(33, 24, 14, 0.25);
border: 1px solid rgba(118, 103, 84, 0.25);
}
.exp-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
gap: 12px;
}
.title {
font-size: 22px;
color: #3d3929;
font-weight: 700;
}
.subtitle {
font-size: 14px;
color: #7f7766;
}
.confirm-btn {
border: none;
background: #da7756;
color: #fff;
border-radius: 12px;
padding: 10px 18px;
font-weight: 600;
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.mode-card {
border: 1px solid rgba(118, 103, 84, 0.25);
border-radius: 14px;
padding: 16px 14px;
cursor: pointer;
transition: .2s ease;
box-shadow: 0 8px 18px rgba(61, 57, 41, 0.06);
background: #fff;
}
.mode-card:hover {
transform: translateY(-2px);
box-shadow: 0 16px 30px rgba(61, 57, 41, 0.12);
}
.mode-card.active {
border-color: #da7756;
box-shadow: 0 18px 36px rgba(218, 119, 86, 0.25);
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #bd5d3a;
background: rgba(218, 119, 86, 0.14);
padding: 4px 10px;
border-radius: 20px;
margin-bottom: 8px;
}
.icon {
width: 52px;
height: 52px;
border-radius: 14px;
background: rgba(218, 119, 86, 0.12);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10px;
color: #bd5d3a;
}
.icon svg {
width: 30px;
height: 30px;
}
.mode-card h3 {
margin: 0 0 6px;
font-size: 17px;
color: #3d3929;
}
.mode-card p {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: #7f7766;
}
</style>

View File

@ -636,6 +636,9 @@
font-variant-ligatures: none; font-variant-ligatures: none;
font-feature-settings: 'liga' 0, 'calt' 0; font-feature-settings: 'liga' 0, 'calt' 0;
line-height: 1.55; line-height: 1.55;
overflow-x: auto;
max-width: 100%;
scrollbar-gutter: stable both-edges;
} }
.text-output pre code { .text-output pre code {
@ -777,6 +780,9 @@
font-variant-ligatures: none; font-variant-ligatures: none;
font-feature-settings: 'liga' 0, 'calt' 0; font-feature-settings: 'liga' 0, 'calt' 0;
line-height: 1.55; line-height: 1.55;
overflow-x: auto;
max-width: 100%;
scrollbar-gutter: stable both-edges;
} }
.code-block-wrapper pre code { .code-block-wrapper pre code {