Compare commits

..

10 Commits

30 changed files with 619 additions and 358 deletions

View File

@ -72,7 +72,7 @@ window.$toast = showToast;
<style lang="scss">
.app {
display: flex;
width: 100vw;
width: 100%;
height: 100vh;
overflow: hidden;
background: #f5f5f5;

View File

@ -263,7 +263,7 @@ if (typeof window !== "undefined") {
}
.learning-mode-label {
font-size: 13px;
font-size: 14px;
color: #6b7280;
user-select: none;
white-space: nowrap;

View File

@ -516,6 +516,7 @@ watch(
.chat-main {
display: flex;
flex-direction: column;
min-width: 900px;
flex: 1;
height: 100vh;
background: #ffffff;
@ -545,8 +546,7 @@ watch(
}
.input-container {
margin: auto;
width: 55%;
margin: 0 22%;
// min-width: 1000px;
// margin: 0 auto;
transition: max-width 0.3s ease;

View File

@ -13,11 +13,7 @@
<button class="action-btn cancel" @click="handleCancelSelect">
取消
</button>
<button
class="action-btn confirm"
:disabled="selectedMessageCount === 0"
@click="handleConfirmShare"
>
<button class="action-btn confirm" :disabled="selectedMessageCount === 0" @click="handleConfirmShare">
确认分享
</button>
</div>
@ -26,36 +22,21 @@
<div ref="containerRef" class="message-list" @scroll="handleScroll">
<!-- 欢迎界面 -->
<WelcomeScreen
v-if="visibleMessages.length === 0"
@select="$emit('select-suggestion', $event)"
/>
<WelcomeScreen v-if="visibleMessages.length === 0" @select="$emit('select-suggestion', $event)" />
<!-- 消息列表 -->
<template v-else>
<div class="messages-wrapper">
<TransitionGroup name="message">
<MessageBubble
v-for="(message, index) in visibleMessages"
:key="message.id"
:message="message"
:show-timestamp="showTimestamp"
:compact="compact"
:is-New="index === visibleMessages.length - 1"
:is-message-select-mode="isMessageSelectMode"
:is-selected="isMessageSelected(message.id)"
@retry="$emit('retry', message.id)"
@regenerate="$emit('regenerate', message.id)"
@copy="handleCopy(message)"
@like="handleLike(message)"
@dislike="handleDislike(message)"
@select-suggestion="$emit('select-suggestion', $event)"
@preview-image="handlePreviewImage"
@play-video="handlePlayVideo"
@download-file="handleDownloadFile"
<MessageBubble v-for="(message, index) in visibleMessages" :key="message.id" :message="message"
:show-timestamp="showTimestamp" :compact="compact" :is-New="index === visibleMessages.length - 1"
:is-message-select-mode="isMessageSelectMode" :is-selected="isMessageSelected(message.id)"
@retry="$emit('retry', message.id)" @regenerate="$emit('regenerate', message.id)"
@copy="handleCopy(message)" @like="handleLike(message)" @dislike="handleDislike(message)"
@select-suggestion="$emit('select-suggestion', $event)" @preview-image="handlePreviewImage"
@play-video="handlePlayVideo" @download-file="handleDownloadFile"
@toggle-select="handleToggleMessageSelect(message.id)"
@enter-select-mode="handleEnterSelectMode(message.id)"
/>
@enter-select-mode="handleEnterSelectMode(message.id)" />
</TransitionGroup>
<!-- 正在输入指示器 -->
@ -75,11 +56,7 @@
</div>
<!-- 回到底部按钮 -->
<Transition name="fade">
<button
v-if="showScrollButton"
class="scroll-bottom-btn"
@click="handleScrollToBottom"
>
<button v-if="showScrollButton" class="scroll-bottom-btn" @click="handleScrollToBottom">
<ChevronDown :size="20" />
<span v-if="newMessageCount > 0" class="new-count">
{{ newMessageCount }}
@ -359,6 +336,7 @@ onMounted(() => {
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
@ -366,7 +344,7 @@ onMounted(() => {
}
.typing-text {
font-size: 13px;
font-size: 14px;
color: #9ca3af;
}
@ -451,6 +429,7 @@ onMounted(() => {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
@ -458,12 +437,14 @@ onMounted(() => {
}
@keyframes typingBounce {
0%,
80%,
100% {
transform: scale(0.7);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
@ -512,7 +493,7 @@ onMounted(() => {
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;

View File

@ -112,6 +112,11 @@ const iconMap: Record<string, any> = {
学术: ThesisIcon,
};
const excludedSuggestionTexts = new Set([
"让可学 AI 成为我的全科学习导师?",
"让 AI 扮演一位严谨的学术论文写作导师?",
]);
const suggestions = computed(() => {
// icon
type SuggestionWithIcon = Suggestion & { iconComponent: typeof Code };
@ -121,6 +126,8 @@ const suggestions = computed(() => {
// prompt.json
for (const category of Object.values(promptData)) {
for (const [text, systemPrompt] of Object.entries(category)) {
if (excludedSuggestionTexts.has(text)) continue;
//
let iconComponent = Code; //
for (const [keyword, icon] of Object.entries(iconMap)) {
@ -148,8 +155,8 @@ const suggestions = computed(() => {
flex-direction: column;
align-items: center;
justify-content: space-between;
min-height: 80%;
padding: 25px 24px;
min-height: 70%;
padding: 25px 22%;
animation: fadeIn 0.5s ease;
}
@ -272,7 +279,7 @@ const suggestions = computed(() => {
p {
margin: 0;
font-size: 13px;
font-size: 14px;
color: #6b7280;
line-height: 1.5;
@ -317,7 +324,7 @@ const suggestions = computed(() => {
padding: 12px 20px;
background: #F8F9FA;
border: 1px solid transparent;
border-radius: 10px;
border-radius: 15px;
font-size: 12px;
text-align: left;
cursor: pointer;
@ -330,12 +337,10 @@ const suggestions = computed(() => {
}
// TODO:
&:hover {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.05);
background: #E9EAEB;
.arrow-icon {
transform: translateX(4px);
color: #3b82f6;
}
}
@ -367,7 +372,7 @@ const suggestions = computed(() => {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-size: 14px;
color: #9ca3af;
kbd {

View File

@ -1,8 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="15" viewBox="0 0 14 15" fill="none">
<path d="M0.5 2.9H2.9M13.3 2.9H10.9M10.9 2.9V2.5C10.9 1.39543 10.0046 0.5 8.9 0.5H4.9C3.79543 0.5 2.9 1.39543 2.9 2.5V2.9M10.9 2.9H2.9" stroke="#666666" stroke-linecap="round"/>
<path d="M2.09985 5.3V12.1C2.09985 13.2046 2.99528 14.1 4.09985 14.1H9.69986C10.8044 14.1 11.6999 13.2046 11.6999 12.1V5.3M5.29985 6.1V12.5M8.49985 6.1V12.5" stroke="#666666" stroke-linecap="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M1.6001 3.19999H4.0001M14.4001 3.19999H12.0001M12.0001 3.19999V2.79999C12.0001 1.69542 11.1047 0.799988 10.0001 0.799988H6.0001C4.89553 0.799988 4.0001 1.69542 4.0001 2.79999V3.19999M12.0001 3.19999H4.0001" stroke="currentColor" stroke-linecap="round" />
<path d="M3.19995 5.59998V12.4C3.19995 13.5045 4.09538 14.4 5.19995 14.4H10.8C11.9045 14.4 12.8 13.5045 12.8 12.4V5.59998M6.39995 6.39998V12.8M9.59995 6.39998V12.8" stroke="currentColor" stroke-linecap="round" />
</svg>
</template>
<script setup lang="ts">

View File

@ -1,8 +1,8 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M11.7854 6.40508L3.71021 14.4783H1.51099V12.2791L9.58423 4.2039L11.7854 6.40508ZM12.3801 1.52031C12.4497 1.52042 12.5204 1.54921 12.5735 1.60234L14.387 3.41484C14.4383 3.46622 14.467 3.53595 14.467 3.6082C14.467 3.66395 14.4509 3.71702 14.4211 3.76152L14.387 3.80254L13.3225 4.86699L11.1213 2.66582L12.1858 1.60234L12.1887 1.60039C12.2399 1.5487 12.3071 1.52031 12.3801 1.52031Z" stroke="#666666" stroke-linejoin="round"/>
<path d="M6 14.5H14.5" stroke="#666666" stroke-linecap="round"/>
</svg>
<path d="M11.7854 6.40509L3.71021 14.4783H1.51099V12.2791L9.58423 4.20392L11.7854 6.40509ZM12.3801 1.52032C12.4497 1.52043 12.5204 1.54923 12.5735 1.60236L14.387 3.41486C14.4383 3.46624 14.467 3.53597 14.467 3.60822C14.467 3.66397 14.4509 3.71704 14.4211 3.76154L14.387 3.80255L13.3225 4.867L11.1213 2.66583L12.1858 1.60236L12.1887 1.6004C12.2399 1.54872 12.3071 1.52032 12.3801 1.52032Z" stroke="currentColor" stroke-linejoin="round" />
<path d="M6 14.5H14.5" stroke="currentColor" stroke-linecap="round" />
</svg>
</template>
<script setup lang="ts">

View File

@ -0,0 +1,40 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
>
<path
d="M7 13C10.3137 13 13 10.3137 13 7C13 3.68629 10.3137 1 7 1C3.68629 1 1 3.68629 1 7"
stroke="url(#paint0_linear_169_1705)"
stroke-width="2"
stroke-linecap="round"
/>
<defs>
<linearGradient
id="paint0_linear_169_1705"
x1="1"
y1="7"
x2="7"
y2="13"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="white" />
<stop offset="1" stop-color="white" />
</linearGradient>
</defs>
</svg>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
size?: number;
}>(),
{
size: 14,
},
);
</script>

View File

@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M5.43994 0.5H5.44189L7.99268 0.507812H8.0083L10.5581 0.5H10.5601C10.6453 0.500024 10.7177 0.541283 10.7603 0.600586L10.7925 0.666016C10.8015 0.697293 10.8004 0.716064 10.7954 0.732422C10.7899 0.750047 10.767 0.804834 10.6704 0.886719L10.6411 0.914062L10.0366 1.51758C9.84509 1.70911 9.74857 1.9731 9.76904 2.23926L9.77002 2.24121L10.0864 6.15332V6.1543C10.1201 6.56503 10.2691 6.95616 10.5151 7.28418L10.6265 7.4209L11.5933 8.5127C11.6397 8.56497 11.6702 8.62659 11.6841 8.68945L11.6929 8.75293C11.6937 8.78175 11.6899 8.8027 11.686 8.81641C11.6827 8.82804 11.6786 8.83521 11.6733 8.8418C11.6651 8.85078 11.647 8.8623 11.6216 8.8623H11.6206L8.87158 8.84277H7.12842L4.37939 8.8623H4.37842C4.34895 8.8623 4.33235 8.84936 4.32764 8.84375L4.32666 8.84277C4.32122 8.83606 4.31665 8.82825 4.31299 8.81543C4.30892 8.80101 4.30578 8.77903 4.30615 8.74902C4.30865 8.66986 4.34102 8.58452 4.40479 8.5127H4.40576L5.37256 7.4209C5.68385 7.06918 5.8747 6.62516 5.91162 6.15332L6.229 2.24121V2.23926C6.24675 2.00853 6.1773 1.77409 6.02783 1.58984L5.95947 1.51465L5.34521 0.900391L5.32959 0.886719L5.27002 0.831055C5.22258 0.780136 5.2087 0.745665 5.20459 0.732422C5.19958 0.716074 5.19847 0.697262 5.20752 0.666016C5.23456 0.573903 5.32611 0.500071 5.43994 0.5Z"
stroke="currentColor"
/>
<path d="M8 9V15.5" stroke="currentColor" stroke-linecap="round" />
</svg>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
size?: number;
}>(),
{
size: 18,
},
);
</script>

View File

@ -0,0 +1,27 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<path
d="M5.43994 0.5H5.44189L7.99268 0.507812H8.0083L10.5581 0.5H10.5601C10.6453 0.500024 10.7177 0.541283 10.7603 0.600586L10.7925 0.666016C10.8015 0.697293 10.8004 0.716064 10.7954 0.732422C10.7899 0.750047 10.767 0.804834 10.6704 0.886719L10.6411 0.914062L10.0366 1.51758C9.84509 1.70911 9.74857 1.9731 9.76904 2.23926L9.77002 2.24121L10.0864 6.15332V6.1543C10.1201 6.56503 10.2691 6.95616 10.5151 7.28418L10.6265 7.4209L11.5933 8.5127C11.6397 8.56497 11.6702 8.62659 11.6841 8.68945L11.6929 8.75293C11.6937 8.78175 11.6899 8.8027 11.686 8.81641C11.6827 8.82804 11.6786 8.83521 11.6733 8.8418C11.6651 8.85078 11.647 8.8623 11.6216 8.8623H11.6206L8.87158 8.84277H7.12842L4.37939 8.8623H4.37842C4.34895 8.8623 4.33235 8.84936 4.32764 8.84375L4.32666 8.84277C4.32122 8.83606 4.31665 8.82825 4.31299 8.81543C4.30892 8.80101 4.30578 8.77903 4.30615 8.74902C4.30865 8.66986 4.34102 8.58452 4.40479 8.5127H4.40576L5.37256 7.4209C5.68385 7.06918 5.8747 6.62516 5.91162 6.15332L6.229 2.24121V2.23926C6.24675 2.00853 6.1773 1.77409 6.02783 1.58984L5.95947 1.51465L5.34521 0.900391L5.32959 0.886719L5.27002 0.831055C5.22258 0.780136 5.2087 0.745665 5.20459 0.732422C5.19958 0.716074 5.19847 0.697262 5.20752 0.666016C5.23456 0.573903 5.32611 0.500071 5.43994 0.5Z"
stroke="currentColor"
/>
<path d="M8 9V15.5" stroke="currentColor" stroke-linecap="round" />
<path d="M12 4L8 8L4 12" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
size?: number;
}>(),
{
size: 18,
},
);
</script>

View File

@ -1,7 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13" fill="none">
<path d="M0.5 6.49457H12.5" stroke="#999999" stroke-linecap="round" />
<path d="M6.49414 12.5L6.49414 0.5" stroke="#999999" stroke-linecap="round" />
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11" fill="none">
<path d="M0.5 5.49469H10.5" stroke="#999999" stroke-linecap="round" />
<path d="M5.49512 10.5L5.49512 0.5" stroke="#999999" stroke-linecap="round" />
</svg>
</template>

View File

@ -1,18 +1,11 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<g clip-path="url(#clip0_75_622)">
<circle cx="10.5" cy="2.5" r="2" stroke="#666666"/>
<circle cx="3.5" cy="7.5" r="2" stroke="#666666"/>
<path d="M8.79367 3.71881L5.19458 6.28959" stroke="#666666" stroke-linecap="round"/>
<path d="M5 9L9.5 12" stroke="#666666" stroke-linecap="round"/>
<circle cx="12" cy="13" r="2.5" stroke="#666666"/>
</g>
<defs>
<clipPath id="clip0_75_622">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
<circle cx="10.5" cy="2.5" r="2" stroke="currentColor" />
<circle cx="3.5" cy="7.5" r="2" stroke="currentColor" />
<path d="M8.79367 3.71881L5.19458 6.28959" stroke="currentColor" stroke-linecap="round" />
<path d="M5 9L9.5 12" stroke="currentColor" stroke-linecap="round" />
<circle cx="12" cy="13" r="2.5" stroke="currentColor" />
</svg>
</template>
<script setup lang="ts">

View File

@ -0,0 +1,22 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<rect width="12" height="12" rx="2" fill="white" />
</svg>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
size?: number;
}>(),
{
size: 12,
},
);
</script>

View File

@ -8,7 +8,14 @@
@remove="removeAttachment" @add-upload="triggerUploadInput" />
<!-- 隐藏的文件输入框 -->
<input ref="uploadInputRef" type="file" multiple hidden @change="handleUploadSelect" />
<input
ref="uploadInputRef"
type="file"
:accept="uploadAccept"
multiple
hidden
@change="handleUploadSelect"
/>
</div>
<!-- 文本输入框 -->
@ -58,36 +65,35 @@
<!-- 右侧功能按钮 -->
<div class="input-actions right">
<!-- 发送/停止按钮 -->
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
<StopCircle :size="20" />
</button>
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
<Loader2 v-if="isProcessingAttachments" :size="20" class="animate-spin" />
<SendIcon v-else :size="20" />
</button>
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
<StopIcon />
</button>
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
<LoadingIcon v-if="isProcessingAttachments" class="animate-spin" />
<SendIcon v-else :size="20" />
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted } from "vue";
import {
StopCircle,
Sparkles,
Globe,
Brain,
Loader2,
Upload,
} from "@/components/icons";
import { ref, computed, watch, nextTick, onMounted } from "vue";
import {
Sparkles,
Globe,
Brain,
} from "@/components/icons";
import { generateId } from "@/utils/helpers";
import type { Attachment } from "@/types/chat";
import { chatApi } from "@/services/api";
import { useAuthStore } from "@/stores/auth";
import { useSettingsStore } from "@/stores/settings";
import StackedCards from "@/components/ui/StackedCards.vue";
import SendIcon from "../icons/custom/SendIcon.vue";
import { useAuthStore } from "@/stores/auth";
import { useSettingsStore } from "@/stores/settings";
import StackedCards from "@/components/ui/StackedCards.vue";
import SendIcon from "../icons/custom/SendIcon.vue";
import StopIcon from "../icons/custom/StopIcon.vue";
import LoadingIcon from "../icons/custom/LoadingIcon.vue";
interface AttachmentWithProgress extends Attachment {
uploading?: boolean;
@ -175,10 +181,44 @@ function showThrottledToast(message: string, type: "error" = "error") {
//
const charCount = computed(() => inputText.value.length);
const isUploading = computed(() => attachments.value.some((a) => a.uploading));
const isProcessingAttachments = computed(() =>
attachments.value.some((a) => a.uploading || a.deleting),
);
const isUploading = computed(() => attachments.value.some((a) => a.uploading));
const isProcessingAttachments = computed(() =>
attachments.value.some((a) => a.uploading || a.deleting),
);
const textFileAccept =
".txt,.md,.markdown,.pdf,.doc,.docx,.rtf,.csv,.tsv,.json,.xml,.html,.htm,.yaml,.yml,.log,.ini,.conf,.sql,.js,.ts,.jsx,.tsx,.py,.java,.c,.cpp,.h,.hpp,.go,.rs,.sh";
const uploadAccept = computed(() => {
if (props.supports_vision && props.supports_files) {
return `image/*,${textFileAccept}`;
}
if (props.supports_vision) {
return "image/*";
}
if (props.supports_files) {
return textFileAccept;
}
return "";
});
function getFileExt(fileName: string) {
const idx = fileName.lastIndexOf(".");
if (idx === -1) return "";
return fileName.slice(idx).toLowerCase();
}
function getUploadTypeByModel(file: File): "image" | "file" | null {
const isImage =
file.type.startsWith("image/") ||
[".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg", ".heic"].includes(
getFileExt(file.name),
);
if (isImage) {
return props.supports_vision ? "image" : null;
}
return props.supports_files ? "file" : null;
}
const canSend = computed(() => {
return (
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
@ -261,7 +301,7 @@ function handleSend() {
}
//
async function handlePaste(event: ClipboardEvent) {
async function handlePaste(event: ClipboardEvent) {
const items = event.clipboardData?.items;
if (!items) return;
@ -281,31 +321,49 @@ async function handlePaste(event: ClipboardEvent) {
}
}
for (const item of items) {
if (item.type.startsWith("image/")) {
event.preventDefault();
const file = item.getAsFile();
if (file) {
await addFileAsAttachment(file, "image");
}
}
}
}
function triggerUploadInput() {
uploadInputRef.value?.click();
}
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
const uploadType = getUploadTypeByModel(file);
if (uploadType === "image") {
event.preventDefault();
await addFileAsAttachment(file, uploadType);
} else {
event.preventDefault();
showThrottledToast("当前模型不支持上传图片");
}
}
}
}
}
function triggerUploadInput() {
if (!props.supports_vision && !props.supports_files) {
showThrottledToast("当前模型不支持上传附件");
return;
}
uploadInputRef.value?.click();
}
//
async function handleUploadSelect(event: Event) {
const input = event.target as HTMLInputElement;
const files = input.files;
if (!files) return;
for (const file of files) {
const type = file.type.startsWith("image/") ? "image" : "file";
await addFileAsAttachment(file, type);
}
if (!files) return;
for (const file of files) {
const uploadType = getUploadTypeByModel(file);
if (!uploadType) {
showThrottledToast(
file.type.startsWith("image/")
? "当前模型不支持上传图片"
: "当前模型不支持上传附件文件",
);
continue;
}
await addFileAsAttachment(file, uploadType);
}
input.value = "";
}
@ -568,7 +626,7 @@ onMounted(() => {
}
&.send {
background: #e5e7eb;
background: rgba(0, 15, 51, 0.20);
color: #9ca3af;
.dark & {
@ -586,26 +644,26 @@ onMounted(() => {
}
&.loading {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
background: #000F33;
color: white;
cursor: wait;
}
&:disabled {
background: rgba(0, 15, 51, 0.20);
cursor: not-allowed;
opacity: 0.6;
}
}
&.stop {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
animation: pulse 2s infinite;
&:hover {
transform: scale(1.05);
}
}
&.stop {
background: #000F33;
color: white;
&:hover {
transform: scale(1.05);
}
}
}
.textarea-wrapper {
@ -667,7 +725,7 @@ onMounted(() => {
background: var(---FFFFFF, #FFF);
border: 1px solid transparent;
color: var(--6-666666, #666);
font-size: 13px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
@ -703,15 +761,4 @@ onMounted(() => {
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
50% {
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
}
}
</style>
</style>

View File

@ -153,7 +153,7 @@ function handleClick(action: UploadAction) {
}
.card-title {
font-size: 13px;
font-size: 14px;
font-weight: 700;
line-height: 1.1;
}

View File

@ -120,7 +120,7 @@ function toggleExpand() {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-size: 14px;
font-weight: 500;
color: #a6adc8;
@ -170,7 +170,7 @@ function toggleExpand() {
code {
font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace;
font-size: 13px;
font-size: 14px;
line-height: 1.6;
color: #cdd6f4;
tab-size: 2;

View File

@ -276,7 +276,7 @@ if (typeof window !== "undefined") {
border-radius: 8px;
background: transparent;
color: #374151;
font-size: 13px;
font-size: 14px;
text-align: left;
cursor: pointer;
transition: all 0.15s ease;

View File

@ -71,13 +71,22 @@
<!-- 图片展示 -->
<div v-if="message.content.images?.length" class="images-grid">
<div v-for="(image, index) in message.content.images" :key="image.id" class="image-item"
@click="$emit('preview-image', image, index)">
<img :src="image.url" :alt="image.name" loading="lazy" />
<div class="image-overlay">
<Maximize2 :size="18" />
<n-image-group>
<div v-for="image in message.content.images" :key="image.id" class="image-item">
<n-image
class="message-image"
:src="image.url"
object-fit="cover"
:img-props="{
alt: image.name,
loading: 'lazy',
}"
/>
<div class="image-overlay">
<Maximize2 :size="18" />
</div>
</div>
</div>
</n-image-group>
</div>
<!-- 单个视频 -->
@ -121,15 +130,17 @@
</template>
<!-- 加载动画 -->
<!-- <div v-if="message.isStreaming && !message.content.text" class="loading-dots">
<span></span>
<span></span>
<span></span>
</div> -->
<div
v-if="message.role === 'assistant' && message.isStreaming"
class="loading-spinner-row"
aria-label="模型正在生成中"
>
<span class="loading-spinner" aria-hidden="true"></span>
</div>
</div>
<!-- 操作栏 -->
<MessageActions v-if="
<!-- <MessageActions v-if="
message.role === 'assistant' &&
!message.isStreaming &&
!message.isError &&
@ -137,7 +148,7 @@
!isMessageSelectMode
" :content="message.content.text || ''" :feedback="message.feedback" :show-regenerate="true"
:is-hovered="isHovered" :is-new="isNew" :is-break="message.isBreak" @copy="handleCopy" @like="handleLike"
@dislike="handleDislike" @regenerate="$emit('regenerate')" @share="handleShareClick" />
@dislike="handleDislike" @regenerate="$emit('regenerate')" @share="handleShareClick" /> -->
</div>
</div>
</template>
@ -158,6 +169,7 @@ import {
Play,
Check,
} from "@/components/icons";
import { NImage, NImageGroup } from "naive-ui";
import MessageActions from "./MessageActions.vue";
import { formatFileSize, getFileIcon } from "@/utils/helpers";
import type { Message, Suggestion, Attachment, VideoInfo } from "@/types/chat";
@ -361,17 +373,17 @@ setCustomComponents("playground-demo", {
.message-body {
position: relative;
&::after {
content: "";
position: absolute;
bottom: 12px;
right: 12px;
width: 8px;
height: 8px;
background: #3b82f6;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
// &::after {
// content: "";
// position: absolute;
// bottom: 12px;
// right: 12px;
// width: 8px;
// height: 8px;
// background: #3b82f6;
// border-radius: 50%;
// animation: pulse 1.5s infinite;
// }
}
}
}
@ -441,11 +453,9 @@ setCustomComponents("playground-demo", {
// markstream-vue
.text-content {
:deep(p) {
margin: 0 0 12px;
margin: 0 0 16px;
&:last-child {
margin-bottom: 0;
}
}
:deep(ul),
@ -573,7 +583,7 @@ setCustomComponents("playground-demo", {
border-radius: 6px;
background: #ef4444;
color: white;
font-size: 13px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s ease;
@ -599,7 +609,7 @@ setCustomComponents("playground-demo", {
border-radius: 20px;
background: white;
color: #374151;
font-size: 13px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
@ -635,7 +645,12 @@ setCustomComponents("playground-demo", {
overflow: hidden;
cursor: pointer;
img {
:deep(.n-image) {
width: 100%;
height: 100%;
}
:deep(.n-image img) {
width: 100%;
height: 100%;
object-fit: cover;
@ -652,10 +667,11 @@ setCustomComponents("playground-demo", {
color: white;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
&:hover {
img {
:deep(.n-image img) {
transform: scale(1.05);
}
@ -664,6 +680,13 @@ setCustomComponents("playground-demo", {
}
}
}
.images-grid{
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
}
.single-video {
margin-top: 12px;
@ -734,7 +757,7 @@ setCustomComponents("playground-demo", {
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.03);
background: rgba(0, 0, 0, 0.05);
border-radius: 10px;
.dark & {
@ -811,6 +834,26 @@ setCustomComponents("playground-demo", {
}
}
.loading-spinner-row {
display: flex;
align-items: center;
padding: 8px 0 0;
}
.loading-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(102, 102, 102, 0.25);
border-top-color: #666666;
border-radius: 50%;
animation: spin 0.8s linear infinite;
.dark & {
border-color: rgba(255, 255, 255, 0.25);
border-top-color: #f3f4f6;
}
}
@keyframes fadeIn {
from {
opacity: 0;
@ -852,6 +895,12 @@ setCustomComponents("playground-demo", {
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
//
.message-checkbox {
position: absolute;

View File

@ -64,7 +64,7 @@ async function textCopy(data: any) {
</div> -->
<div class="thinking-title">
<!-- TODO: 深度思考样式 -->
<span class="text-lg"> 深度思考</span>
<span > 深度思考</span>
<!-- 加载动画 -->
<span v-if="node.loading" class="thinking-dots visible" aria-hidden="true">
<span class="dot dot-1" />
@ -86,7 +86,7 @@ async function textCopy(data: any) {
<!-- 可折叠的内容区域 -->
<div class="thinking-content" :class="{ collapsed }">
<div class="mt-3 text-sm leading-relaxed dark:text-slate-100">
<div class="mt-3 text-[13px] leading-relaxed dark:text-slate-100">
<MarkdownRender :content="node.content" @copy="textCopy" />
</div>
</div>
@ -108,6 +108,7 @@ async function textCopy(data: any) {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 15px 0;
border-bottom: 1px solid #e2e8f0;
gap: 12px;
cursor: pointer;
@ -159,7 +160,7 @@ line-height: 21px;
.thinking-content {
color: var(--9-999999, #999);
font-family: "Microsoft YaHei";
font-size: 13px;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;

View File

@ -746,7 +746,7 @@ function handleClearData() {
border-radius: 10px;
background: white;
color: #6b7280;
font-size: 13px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
@ -786,7 +786,7 @@ function handleClearData() {
border-radius: 8px;
background: transparent;
color: #6b7280;
font-size: 13px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;

View File

@ -354,7 +354,7 @@ watch(show, (newVal: boolean) => {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-size: 14px;
color: #4b5563;
.dark & {
@ -495,7 +495,7 @@ watch(show, (newVal: boolean) => {
padding: 12px;
background: rgba(59, 130, 246, 0.05);
border-radius: 10px;
font-size: 13px;
font-size: 14px;
color: #3b82f6;
.dark & {

View File

@ -262,7 +262,7 @@ watch(show, (newVal: boolean) => {
.share-section {
.share-label {
display: block;
font-size: 13px;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
@ -282,7 +282,7 @@ watch(show, (newVal: boolean) => {
padding: 12px 14px;
border: 1px solid #e5e7eb;
border-radius: 10px;
font-size: 13px;
font-size: 14px;
color: #1f2937;
background: #f9fafb;
@ -307,7 +307,7 @@ watch(show, (newVal: boolean) => {
border-radius: 10px;
background: #f3f4f6;
color: #374151;
font-size: 13px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
@ -336,7 +336,7 @@ watch(show, (newVal: boolean) => {
padding: 12px;
background: rgba(245, 158, 11, 0.1);
border-radius: 10px;
font-size: 13px;
font-size: 14px;
color: #f59e0b;
}

View File

@ -264,7 +264,7 @@ function close() {
}
.tip {
font-size: 13px;
font-size: 14px;
color: #9ca3af;
kbd {

View File

@ -583,7 +583,7 @@ onBeforeUnmount(() => {
.search-placeholder {
flex: 1;
font-size: 13px;
font-size: 14px;
}
.search-kbd {

View File

@ -39,12 +39,11 @@
<n-tooltip :style="{ borderRadius: '5px', padding: '7px 15px' }">
<template #trigger>
<button
class="action-btn"
class="action-btn pin-toggle-btn"
@click="handleTogglePin"
>
<!-- TODO: 取消置顶图标 -->
<PinOff v-if="conversation.pinned" :size="14" />
<PinIcon v-else :size="14" />
<PinOffActionIcon v-if="conversation.pinned" :size="14" />
<PinActionIcon v-else :size="14" />
</button>
</template>
{{ conversation.pinned ? '取消置顶' : '置顶' }}
@ -83,7 +82,6 @@
<script setup lang="ts">
import { ref, computed, nextTick } from "vue";
import {
PinOff,
Check,
} from "@/components/icons";
import { formatTimestamp } from "@/utils/helpers";
@ -91,6 +89,8 @@ import type { Conversation } from "@/types/chat";
import { NTooltip } from "naive-ui";
import MessageIcon from "../icons/custom/MessageIcon.vue";
import PinIcon from "../icons/custom/PinIcon.vue";
import PinActionIcon from "../icons/custom/PinActionIcon.vue";
import PinOffActionIcon from "../icons/custom/PinOffActionIcon.vue";
import EditIcon from "../icons/custom/EditIcon.vue";
import DeleteIcon from "../icons/custom/DeleteIcon.vue";
import ShareIcon from "../icons/custom/ShareIcon.vue";
@ -210,11 +210,22 @@ function handleDelete() {
.item-title {
font-weight: 700;
}
}
&.active:hover {
.item-actions {
opacity: 1;
pointer-events: auto;
}
.pin-indicator {
opacity: 0;
}
.item-content {
flex: 0 1 clamp(72px, 28%, 96px);
max-width: clamp(72px, 28%, 96px);
}
}
}
@ -315,7 +326,7 @@ function handleDelete() {
&:hover {
background: rgba(0, 0, 0, 0.1);
color: #374151;
color: #000F33;
.dark & {
background: rgba(255, 255, 255, 0.1);
@ -324,8 +335,23 @@ function handleDelete() {
}
&.delete:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
color: #f86361;
}
}
.pin-toggle-btn {
color: #666666;
.dark & {
color: #666666;
}
&:hover {
color: #000f33;
.dark & {
color: #000f33;
}
}
}

View File

@ -74,7 +74,7 @@ function handleShareCurrent() {
}
.select-info {
font-size: 13px;
font-size: 14px;
color: #6b7280;
text-align: center;
@ -92,7 +92,7 @@ function handleShareCurrent() {
flex: 1;
padding: 8px 12px;
border-radius: 10px;
font-size: 13px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { NModal, NImage } from 'naive-ui'
import { NImage, NTooltip } from 'naive-ui'
import PlusIcon from '../icons/custom/PlusIcon.vue'
export interface CardItem {
id: string | number
@ -42,16 +42,7 @@ const isExpanded = ref(false)
const isWrapperHovered = ref(false)
const hoveredCardId = ref<string | number | null>(null)
const containerRef = ref<HTMLElement | null>(null)
const previewCard = ref<CardItem | null>(null)
const canUpload = computed(() => props.supportsFiles || props.supportsVision)
const isPreviewVisible = computed({
get: () => !!previewCard.value,
set: (value: boolean) => {
if (!value) {
closePreview()
}
},
})
const CARD_SCALE = 0.5
// -
@ -138,21 +129,6 @@ function getCardImageUrl(card: CardItem) {
return card.thumbnail || ''
}
function openPreview(card: CardItem) {
if (card.type !== 'image') return
const imageUrl = getCardImageUrl(card)
if (!imageUrl) return
previewCard.value = card
}
function closePreview() {
previewCard.value = null
}
function removePreviewCard() {
closePreview()
}
function removeCard(card: CardItem) {
emit('remove', card.id)
}
@ -170,10 +146,6 @@ function handleCardClick(card: CardItem) {
expandCards()
return
}
if (card.type === 'image') {
openPreview(card)
}
}
const cardStyle = computed(() => (index: number, total: number) => {
@ -214,9 +186,21 @@ const cardStyle = computed(() => (index: number, total: number) => {
const handleDocumentClick = (event: MouseEvent) => {
if (!containerRef.value) return
const target = event.target as HTMLElement | null
// Naive UI body
if (
target?.closest(
'.n-image-preview-container, .n-image-preview-toolbar, .n-image-preview-overlay, .n-image-preview-close',
)
) {
return
}
if (!target) return
//
if (!containerRef.value.contains(event.target as Node)) {
if (!containerRef.value.contains(target)) {
if (isExpanded.value) {
isExpanded.value = false
hoveredCardId.value = null
@ -262,58 +246,96 @@ watch(
@click="expandCards">
<div class="cards-wrapper" @mouseenter="isWrapperHovered = true" @mouseleave="isWrapperHovered = false">
<TransitionGroup name="card-spread">
<div v-for="(card, index) in cards" :key="card.id" class="card" :style="cardStyle(index, cards.length)"
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
<div v-if="card.uploading" class="card-media-uploading">
<div class="card-media-uploading-spinner" aria-hidden="true" />
<span class="card-media-uploading-text">上传中</span>
<NTooltip
v-for="(card, index) in cards"
:key="card.id"
trigger="hover"
placement="top"
>
<template #trigger>
<div class="card" :style="cardStyle(index, cards.length)"
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
<NImage
:src="getCardImageUrl(card)"
:alt="getCardTitle(card, index)"
object-fit="cover"
:preview-disabled="!isExpanded || card.uploading || card.deleting"
:img-props="{ loading: 'lazy' }"
/>
<div v-if="card.uploading" class="card-media-uploading">
<div class="card-media-uploading-spinner" aria-hidden="true" />
<span class="card-media-uploading-text">上传中</span>
</div>
<div v-else-if="card.deleting" class="card-media-deleting">
<div class="card-media-deleting-spinner" aria-hidden="true" />
<span class="card-media-deleting-text">删除中</span>
</div>
</div>
<div v-else-if="card.type === 'image'" class="card-preview">
<div v-if="getCardImageUrl(card)" class="card-preview-image">
<NImage
:src="getCardImageUrl(card)"
:alt="getCardTitle(card, index)"
object-fit="cover"
:preview-disabled="!isExpanded || card.uploading || card.deleting"
:img-props="{ loading: 'lazy' }"
/>
</div>
<div class="card-preview-fallback">
<span class="card-icon">
{{ getCardIcon(card) }}
</span>
</div>
<div v-if="card.deleting" class="card-preview-deleting">
<div class="card-preview-deleting-spinner" aria-hidden="true" />
<span class="card-preview-deleting-text">删除中</span>
</div>
</div>
<div v-else class="card-file-preview">
<div class="card-file-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect x="2.5" y="1.5" width="11" height="13" rx="1.5" stroke="#666666" />
<path d="M5 5H11" stroke="#666666" stroke-linecap="round" />
<path d="M5 8H11" stroke="#666666" stroke-linecap="round" />
<path d="M5 11H11" stroke="#666666" stroke-linecap="round" />
</svg>
</div>
<div class="card-file-name" :title="getCardTitle(card, index)">
{{ getCardTitle(card, index) }}
</div>
<div v-if="card.deleting" class="card-preview-deleting">
<div class="card-preview-deleting-spinner" aria-hidden="true" />
<span class="card-preview-deleting-text">删除中</span>
</div>
</div>
<button v-if="!card.deleting && isWrapperHovered && (hoveredCardId === card.id || (!hoveredCardId && index === 0))"
type="button" class="card-delete-btn" title="删除 OSS 文件" @click.stop="removeCard(card)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<rect width="16.0008" height="16" rx="8" fill="#999999" />
<path d="M5.49512 5.33374L10.6692 10.6624" stroke="white" stroke-linecap="round" />
<path d="M10.5068 5.33813L5.33271 10.6668" stroke="white" stroke-linecap="round" />
</svg>
</button>
</div>
<div v-else-if="card.deleting" class="card-media-deleting">
<div class="card-media-deleting-spinner" aria-hidden="true" />
<span class="card-media-deleting-text">删除中</span>
</div>
</div>
<div v-else class="card-preview">
<div v-if="getCardImageUrl(card)" class="card-preview-image">
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
</div>
<div class="card-preview-fallback">
<span class="card-icon">
{{ getCardIcon(card) }}
</span>
</div>
<div v-if="card.deleting" class="card-preview-deleting">
<div class="card-preview-deleting-spinner" aria-hidden="true" />
<span class="card-preview-deleting-text">删除中</span>
</div>
</div>
<button v-if="!card.deleting && isWrapperHovered && (hoveredCardId === card.id || (!hoveredCardId && index === 0))"
type="button" class="card-delete-btn" title="删除 OSS 文件" @click.stop="removeCard(card)">
</button>
</div>
</template>
{{ getCardTitle(card, index) }}
</NTooltip>
</TransitionGroup>
</div>
<NModal v-model:show="isPreviewVisible" preset="card" :mask-closable="true" :close-on-esc="true"
:on-close="removePreviewCard" class="image-preview-modal"
:content-style="{ width: '100%', height: '100%', padding: '0', overflow: 'hidden' }"
:style="{ width: '100vw', height: '100vh', maxWidth: '100vw', margin: '0', padding: '0', borderRadius: '0', background: 'rgba(5, 7, 12, 0.96)' }">
<div class="image-preview-shell">
<NImage v-if="previewCard" :src="getCardImageUrl(previewCard)" :alt="getCardTitle(previewCard, 0)"
object-fit="contain" preview-disabled :img-props="{
style: {
width: '100%',
height: '100%',
maxWidth: '100%',
maxHeight: '100%',
},
}" class="image-preview-content" />
</div>
</NModal>
<button
v-if="!isExpanded"
type="button"
class="cards-upload-fab"
:class="{ disabled: !canUpload }"
:disabled="!canUpload"
:title="canUpload ? '上传附件或图片' : '当前模型不支持上传'"
@click.stop="emit('add-upload')"
>
<PlusIcon :size="12" />
</button>
</div>
</template>
@ -329,6 +351,35 @@ watch(
perspective: 1000px;
}
.cards-upload-fab {
position: absolute;
right: 2px;
bottom: 2px;
z-index: 999;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: 1px solid #e5e7eb;
border-radius: 999px;
background: #ffffff;
color: #999999;
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.16);
cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
}
.cards-upload-fab:hover:not(:disabled) {
transform: scale(1.06);
}
.cards-upload-fab.disabled,
.cards-upload-fab:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.stacked-cards-empty {
display: flex;
align-items: center;
@ -348,17 +399,18 @@ watch(
position: absolute;
width: 74%;
height: 100%;
border-radius: 5px;
border-radius: 10px;
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
cursor: pointer;
overflow: hidden;
/* overflow: hidden; */
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
will-change: transform, opacity;
}
.card-media,
.card-preview {
.card-preview,
.card-file-preview {
position: absolute;
inset: 0;
z-index: 0;
@ -366,6 +418,8 @@ watch(
.card-media {
cursor: default;
border-radius: 10px;
overflow: hidden;
}
.card-media-uploading {
@ -456,33 +510,41 @@ watch(
.card-delete-btn {
position: absolute;
top: 0.35rem;
right: 0.35rem;
top: -0.3rem;
right: -0.3rem;
z-index: 4;
display: inline-flex;
align-items: center;
justify-content: center;
width: 0.95rem;
height: 0.95rem;
width: 16.001px;
height: 16px;
aspect-ratio: 1 / 1;
padding: 0;
border: 0;
border-radius: 999px;
background: rgba(239, 68, 68, 0.9);
color: #fff;
font-size: 0.55rem;
line-height: 1;
border-radius: 50%;
background: transparent;
cursor: pointer;
backdrop-filter: blur(10px);
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.card-delete-btn:hover {
transform: scale(1.08);
background: rgba(220, 38, 38, 0.98);
}
.card-media img,
.card-preview-image img {
.card-delete-btn svg {
width: 16.001px;
height: 16px;
display: block;
}
.card-media :deep(.n-image),
.card-preview-image :deep(.n-image) {
width: 100%;
height: 100%;
}
.card-media :deep(.n-image img),
.card-preview-image :deep(.n-image img) {
width: 100%;
height: 100%;
object-fit: cover;
@ -555,6 +617,38 @@ watch(
font-size: 0.95rem;
}
.card-file-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 6px;
background: #fff;
}
.card-file-icon {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.card-file-name {
width: 100%;
color: var(--6-666666, #666);
font-family: "Microsoft YaHei";
font-size: 10px;
font-style: normal;
font-weight: 400;
line-height: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.card:hover .card-glow {
opacity: 0.15;
@ -709,61 +803,6 @@ watch(
transform: translateX(-50%) translateY(6px);
}
.image-preview-modal :deep(.n-card) {
background: transparent;
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.image-preview-modal :deep(.n-card__content) {
flex: 1;
min-width: 0;
min-height: 0;
width: 100%;
height: 100%;
padding: 0;
overflow: hidden;
display: flex;
}
.image-preview-shell {
position: relative;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-preview-content {
flex: 1;
width: 100%;
height: 100%;
max-width: calc(100vw - 48px);
max-height: calc(100vh - 48px);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
z-index: 1;
}
.image-preview-content :deep(img) {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
}
/* 响应式 */
@media (max-width: 640px) {
.stacked-cards-container {

View File

@ -39,7 +39,7 @@
--header-height: 60px;
--app-text-color: #333;
--app-font-family: "Microsoft YaHei", sans-serif;
--app-font-size: 12px;
--app-font-size: 14px;
--app-font-style: normal;
--app-font-weight: 400;
--app-line-height: normal;
@ -91,3 +91,7 @@ body,
opacity: 0;
transform: translateX(-20px);
}
hr.hr-node[custom-id="playground-demo"]{
margin: 1rem 0;
}

View File

@ -85,6 +85,7 @@ authStore.init()
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
}
</style>
</style>

View File

@ -347,7 +347,7 @@ onMounted(() => {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-size: 14px;
color: #6b7280;
&.expired {
@ -411,7 +411,7 @@ onMounted(() => {
}
.verify-error {
font-size: 13px;
font-size: 14px;
color: #ef4444;
margin: 0;
}
@ -482,7 +482,7 @@ onMounted(() => {
}
.conversation-count {
font-size: 13px;
font-size: 14px;
color: #6b7280;
}