Compare commits
10 Commits
9abe247503
...
747249356d
| Author | SHA1 | Date |
|---|---|---|
|
|
747249356d | |
|
|
375a7bd16e | |
|
|
3bab2b008f | |
|
|
32aa5d0f69 | |
|
|
db13c1a4e0 | |
|
|
c787802352 | |
|
|
135f3ae5c8 | |
|
|
88e43c1763 | |
|
|
47adf9d2cf | |
|
|
0dd6c4f712 |
|
|
@ -72,7 +72,7 @@ window.$toast = showToast;
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100vw;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ if (typeof window !== "undefined") {
|
||||||
}
|
}
|
||||||
|
|
||||||
.learning-mode-label {
|
.learning-mode-label {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,7 @@ watch(
|
||||||
.chat-main {
|
.chat-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-width: 900px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
@ -545,8 +546,7 @@ watch(
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-container {
|
.input-container {
|
||||||
margin: auto;
|
margin: 0 22%;
|
||||||
width: 55%;
|
|
||||||
// min-width: 1000px;
|
// min-width: 1000px;
|
||||||
// margin: 0 auto;
|
// margin: 0 auto;
|
||||||
transition: max-width 0.3s ease;
|
transition: max-width 0.3s ease;
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,7 @@
|
||||||
<button class="action-btn cancel" @click="handleCancelSelect">
|
<button class="action-btn cancel" @click="handleCancelSelect">
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="action-btn confirm" :disabled="selectedMessageCount === 0" @click="handleConfirmShare">
|
||||||
class="action-btn confirm"
|
|
||||||
:disabled="selectedMessageCount === 0"
|
|
||||||
@click="handleConfirmShare"
|
|
||||||
>
|
|
||||||
确认分享
|
确认分享
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -26,36 +22,21 @@
|
||||||
|
|
||||||
<div ref="containerRef" class="message-list" @scroll="handleScroll">
|
<div ref="containerRef" class="message-list" @scroll="handleScroll">
|
||||||
<!-- 欢迎界面 -->
|
<!-- 欢迎界面 -->
|
||||||
<WelcomeScreen
|
<WelcomeScreen v-if="visibleMessages.length === 0" @select="$emit('select-suggestion', $event)" />
|
||||||
v-if="visibleMessages.length === 0"
|
|
||||||
@select="$emit('select-suggestion', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="messages-wrapper">
|
<div class="messages-wrapper">
|
||||||
<TransitionGroup name="message">
|
<TransitionGroup name="message">
|
||||||
<MessageBubble
|
<MessageBubble v-for="(message, index) in visibleMessages" :key="message.id" :message="message"
|
||||||
v-for="(message, index) in visibleMessages"
|
:show-timestamp="showTimestamp" :compact="compact" :is-New="index === visibleMessages.length - 1"
|
||||||
:key="message.id"
|
:is-message-select-mode="isMessageSelectMode" :is-selected="isMessageSelected(message.id)"
|
||||||
:message="message"
|
@retry="$emit('retry', message.id)" @regenerate="$emit('regenerate', message.id)"
|
||||||
:show-timestamp="showTimestamp"
|
@copy="handleCopy(message)" @like="handleLike(message)" @dislike="handleDislike(message)"
|
||||||
:compact="compact"
|
@select-suggestion="$emit('select-suggestion', $event)" @preview-image="handlePreviewImage"
|
||||||
:is-New="index === visibleMessages.length - 1"
|
@play-video="handlePlayVideo" @download-file="handleDownloadFile"
|
||||||
: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)"
|
@toggle-select="handleToggleMessageSelect(message.id)"
|
||||||
@enter-select-mode="handleEnterSelectMode(message.id)"
|
@enter-select-mode="handleEnterSelectMode(message.id)" />
|
||||||
/>
|
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
|
|
||||||
<!-- 正在输入指示器 -->
|
<!-- 正在输入指示器 -->
|
||||||
|
|
@ -75,11 +56,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- 回到底部按钮 -->
|
<!-- 回到底部按钮 -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<button
|
<button v-if="showScrollButton" class="scroll-bottom-btn" @click="handleScrollToBottom">
|
||||||
v-if="showScrollButton"
|
|
||||||
class="scroll-bottom-btn"
|
|
||||||
@click="handleScrollToBottom"
|
|
||||||
>
|
|
||||||
<ChevronDown :size="20" />
|
<ChevronDown :size="20" />
|
||||||
<span v-if="newMessageCount > 0" class="new-count">
|
<span v-if="newMessageCount > 0" class="new-count">
|
||||||
{{ newMessageCount }}
|
{{ newMessageCount }}
|
||||||
|
|
@ -359,6 +336,7 @@ onMounted(() => {
|
||||||
&:nth-child(1) {
|
&:nth-child(1) {
|
||||||
animation-delay: -0.32s;
|
animation-delay: -0.32s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:nth-child(2) {
|
&:nth-child(2) {
|
||||||
animation-delay: -0.16s;
|
animation-delay: -0.16s;
|
||||||
}
|
}
|
||||||
|
|
@ -366,7 +344,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.typing-text {
|
.typing-text {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,6 +429,7 @@ onMounted(() => {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
|
@ -458,12 +437,14 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes typingBounce {
|
@keyframes typingBounce {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
80%,
|
80%,
|
||||||
100% {
|
100% {
|
||||||
transform: scale(0.7);
|
transform: scale(0.7);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
40% {
|
40% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -512,7 +493,7 @@ onMounted(() => {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,11 @@ const iconMap: Record<string, any> = {
|
||||||
学术: ThesisIcon,
|
学术: ThesisIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const excludedSuggestionTexts = new Set([
|
||||||
|
"让可学 AI 成为我的全科学习导师?",
|
||||||
|
"让 AI 扮演一位严谨的学术论文写作导师?",
|
||||||
|
]);
|
||||||
|
|
||||||
const suggestions = computed(() => {
|
const suggestions = computed(() => {
|
||||||
// 内部类型,包含 icon 组件用于渲染
|
// 内部类型,包含 icon 组件用于渲染
|
||||||
type SuggestionWithIcon = Suggestion & { iconComponent: typeof Code };
|
type SuggestionWithIcon = Suggestion & { iconComponent: typeof Code };
|
||||||
|
|
@ -121,6 +126,8 @@ const suggestions = computed(() => {
|
||||||
// 遍历 prompt.json 的分类和条目
|
// 遍历 prompt.json 的分类和条目
|
||||||
for (const category of Object.values(promptData)) {
|
for (const category of Object.values(promptData)) {
|
||||||
for (const [text, systemPrompt] of Object.entries(category)) {
|
for (const [text, systemPrompt] of Object.entries(category)) {
|
||||||
|
if (excludedSuggestionTexts.has(text)) continue;
|
||||||
|
|
||||||
// 根据文本关键词选择图标
|
// 根据文本关键词选择图标
|
||||||
let iconComponent = Code; // 默认图标
|
let iconComponent = Code; // 默认图标
|
||||||
for (const [keyword, icon] of Object.entries(iconMap)) {
|
for (const [keyword, icon] of Object.entries(iconMap)) {
|
||||||
|
|
@ -148,8 +155,8 @@ const suggestions = computed(() => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 80%;
|
min-height: 70%;
|
||||||
padding: 25px 24px;
|
padding: 25px 22%;
|
||||||
animation: fadeIn 0.5s ease;
|
animation: fadeIn 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +279,7 @@ const suggestions = computed(() => {
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
|
|
@ -317,7 +324,7 @@ const suggestions = computed(() => {
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
background: #F8F9FA;
|
background: #F8F9FA;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 10px;
|
border-radius: 15px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -330,12 +337,10 @@ const suggestions = computed(() => {
|
||||||
}
|
}
|
||||||
// TODO: 悬浮边框和背景颜色
|
// TODO: 悬浮边框和背景颜色
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #3b82f6;
|
background: #E9EAEB;
|
||||||
background: rgba(59, 130, 246, 0.05);
|
|
||||||
|
|
||||||
.arrow-icon {
|
.arrow-icon {
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
color: #3b82f6;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,7 +372,7 @@ const suggestions = computed(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="15" viewBox="0 0 14 15" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" 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="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="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"/>
|
<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>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<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="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="#666666" stroke-linecap="round"/>
|
<path d="M6 14.5H14.5" stroke="currentColor" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 11 11" fill="none">
|
||||||
<path d="M0.5 6.49457H12.5" stroke="#999999" stroke-linecap="round" />
|
<path d="M0.5 5.49469H10.5" stroke="#999999" stroke-linecap="round" />
|
||||||
<path d="M6.49414 12.5L6.49414 0.5" stroke="#999999" stroke-linecap="round" />
|
<path d="M5.49512 10.5L5.49512 0.5" stroke="#999999" stroke-linecap="round" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<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="currentColor" />
|
||||||
<circle cx="10.5" cy="2.5" r="2" stroke="#666666"/>
|
<circle cx="3.5" cy="7.5" r="2" stroke="currentColor" />
|
||||||
<circle cx="3.5" cy="7.5" r="2" stroke="#666666"/>
|
<path d="M8.79367 3.71881L5.19458 6.28959" stroke="currentColor" stroke-linecap="round" />
|
||||||
<path d="M8.79367 3.71881L5.19458 6.28959" stroke="#666666" stroke-linecap="round"/>
|
<path d="M5 9L9.5 12" stroke="currentColor" stroke-linecap="round" />
|
||||||
<path d="M5 9L9.5 12" stroke="#666666" stroke-linecap="round"/>
|
<circle cx="12" cy="13" r="2.5" stroke="currentColor" />
|
||||||
<circle cx="12" cy="13" r="2.5" stroke="#666666"/>
|
</svg>
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_75_622">
|
|
||||||
<rect width="16" height="16" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -8,7 +8,14 @@
|
||||||
@remove="removeAttachment" @add-upload="triggerUploadInput" />
|
@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>
|
</div>
|
||||||
|
|
||||||
<!-- 文本输入框 -->
|
<!-- 文本输入框 -->
|
||||||
|
|
@ -59,11 +66,11 @@
|
||||||
<div class="input-actions right">
|
<div class="input-actions right">
|
||||||
<!-- 发送/停止按钮 -->
|
<!-- 发送/停止按钮 -->
|
||||||
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
|
<button v-if="isStreaming" class="action-btn stop" title="停止生成" @click="$emit('stop')">
|
||||||
<StopCircle :size="20" />
|
<StopIcon />
|
||||||
</button>
|
</button>
|
||||||
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
|
<button v-else class="action-btn send" :class="{ active: canSend, loading: isProcessingAttachments }"
|
||||||
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
:disabled="!canSend" :title="isProcessingAttachments ? '附件处理中...' : '发送消息 (Ctrl+Enter)'" @click="handleSend">
|
||||||
<Loader2 v-if="isProcessingAttachments" :size="20" class="animate-spin" />
|
<LoadingIcon v-if="isProcessingAttachments" class="animate-spin" />
|
||||||
<SendIcon v-else :size="20" />
|
<SendIcon v-else :size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,12 +81,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
||||||
import {
|
import {
|
||||||
StopCircle,
|
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Globe,
|
Globe,
|
||||||
Brain,
|
Brain,
|
||||||
Loader2,
|
|
||||||
Upload,
|
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
import { generateId } from "@/utils/helpers";
|
import { generateId } from "@/utils/helpers";
|
||||||
import type { Attachment } from "@/types/chat";
|
import type { Attachment } from "@/types/chat";
|
||||||
|
|
@ -88,6 +92,8 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import { useSettingsStore } from "@/stores/settings";
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
import StackedCards from "@/components/ui/StackedCards.vue";
|
import StackedCards from "@/components/ui/StackedCards.vue";
|
||||||
import SendIcon from "../icons/custom/SendIcon.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 {
|
interface AttachmentWithProgress extends Attachment {
|
||||||
uploading?: boolean;
|
uploading?: boolean;
|
||||||
|
|
@ -179,6 +185,40 @@ const isUploading = computed(() => attachments.value.some((a) => a.uploading));
|
||||||
const isProcessingAttachments = computed(() =>
|
const isProcessingAttachments = computed(() =>
|
||||||
attachments.value.some((a) => a.uploading || a.deleting),
|
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(() => {
|
const canSend = computed(() => {
|
||||||
return (
|
return (
|
||||||
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
|
(inputText.value.trim().length > 0 || attachments.value.length > 0) &&
|
||||||
|
|
@ -283,16 +323,26 @@ async function handlePaste(event: ClipboardEvent) {
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.type.startsWith("image/")) {
|
if (item.type.startsWith("image/")) {
|
||||||
event.preventDefault();
|
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
await addFileAsAttachment(file, "image");
|
const uploadType = getUploadTypeByModel(file);
|
||||||
|
if (uploadType === "image") {
|
||||||
|
event.preventDefault();
|
||||||
|
await addFileAsAttachment(file, uploadType);
|
||||||
|
} else {
|
||||||
|
event.preventDefault();
|
||||||
|
showThrottledToast("当前模型不支持上传图片");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerUploadInput() {
|
function triggerUploadInput() {
|
||||||
|
if (!props.supports_vision && !props.supports_files) {
|
||||||
|
showThrottledToast("当前模型不支持上传附件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
uploadInputRef.value?.click();
|
uploadInputRef.value?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,8 +353,16 @@ async function handleUploadSelect(event: Event) {
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const type = file.type.startsWith("image/") ? "image" : "file";
|
const uploadType = getUploadTypeByModel(file);
|
||||||
await addFileAsAttachment(file, type);
|
if (!uploadType) {
|
||||||
|
showThrottledToast(
|
||||||
|
file.type.startsWith("image/")
|
||||||
|
? "当前模型不支持上传图片"
|
||||||
|
: "当前模型不支持上传附件文件",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await addFileAsAttachment(file, uploadType);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.value = "";
|
input.value = "";
|
||||||
|
|
@ -568,7 +626,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.send {
|
&.send {
|
||||||
background: #e5e7eb;
|
background: rgba(0, 15, 51, 0.20);
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
|
|
@ -586,21 +644,21 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
background: #000F33;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
background: rgba(0, 15, 51, 0.20);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.stop {
|
&.stop {
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
background: #000F33;
|
||||||
color: white;
|
color: white;
|
||||||
animation: pulse 2s infinite;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
|
|
@ -667,7 +725,7 @@ onMounted(() => {
|
||||||
background: var(---FFFFFF, #FFF);
|
background: var(---FFFFFF, #FFF);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: var(--6-666666, #666);
|
color: var(--6-666666, #666);
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
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>
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ function handleClick(action: UploadAction) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ function toggleExpand() {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #a6adc8;
|
color: #a6adc8;
|
||||||
|
|
||||||
|
|
@ -170,7 +170,7 @@ function toggleExpand() {
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace;
|
font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #cdd6f4;
|
color: #cdd6f4;
|
||||||
tab-size: 2;
|
tab-size: 2;
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ if (typeof window !== "undefined") {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
|
|
||||||
|
|
@ -71,13 +71,22 @@
|
||||||
|
|
||||||
<!-- 图片展示 -->
|
<!-- 图片展示 -->
|
||||||
<div v-if="message.content.images?.length" class="images-grid">
|
<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"
|
<n-image-group>
|
||||||
@click="$emit('preview-image', image, index)">
|
<div v-for="image in message.content.images" :key="image.id" class="image-item">
|
||||||
<img :src="image.url" :alt="image.name" loading="lazy" />
|
<n-image
|
||||||
|
class="message-image"
|
||||||
|
:src="image.url"
|
||||||
|
object-fit="cover"
|
||||||
|
:img-props="{
|
||||||
|
alt: image.name,
|
||||||
|
loading: 'lazy',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
<div class="image-overlay">
|
<div class="image-overlay">
|
||||||
<Maximize2 :size="18" />
|
<Maximize2 :size="18" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</n-image-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 单个视频 -->
|
<!-- 单个视频 -->
|
||||||
|
|
@ -121,15 +130,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 加载动画 -->
|
<!-- 加载动画 -->
|
||||||
<!-- <div v-if="message.isStreaming && !message.content.text" class="loading-dots">
|
<div
|
||||||
<span></span>
|
v-if="message.role === 'assistant' && message.isStreaming"
|
||||||
<span></span>
|
class="loading-spinner-row"
|
||||||
<span></span>
|
aria-label="模型正在生成中"
|
||||||
</div> -->
|
>
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作栏 -->
|
<!-- 操作栏 -->
|
||||||
<MessageActions v-if="
|
<!-- <MessageActions v-if="
|
||||||
message.role === 'assistant' &&
|
message.role === 'assistant' &&
|
||||||
!message.isStreaming &&
|
!message.isStreaming &&
|
||||||
!message.isError &&
|
!message.isError &&
|
||||||
|
|
@ -137,7 +148,7 @@
|
||||||
!isMessageSelectMode
|
!isMessageSelectMode
|
||||||
" :content="message.content.text || ''" :feedback="message.feedback" :show-regenerate="true"
|
" :content="message.content.text || ''" :feedback="message.feedback" :show-regenerate="true"
|
||||||
:is-hovered="isHovered" :is-new="isNew" :is-break="message.isBreak" @copy="handleCopy" @like="handleLike"
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -158,6 +169,7 @@ import {
|
||||||
Play,
|
Play,
|
||||||
Check,
|
Check,
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
|
import { NImage, NImageGroup } from "naive-ui";
|
||||||
import MessageActions from "./MessageActions.vue";
|
import MessageActions from "./MessageActions.vue";
|
||||||
import { formatFileSize, getFileIcon } from "@/utils/helpers";
|
import { formatFileSize, getFileIcon } from "@/utils/helpers";
|
||||||
import type { Message, Suggestion, Attachment, VideoInfo } from "@/types/chat";
|
import type { Message, Suggestion, Attachment, VideoInfo } from "@/types/chat";
|
||||||
|
|
@ -361,17 +373,17 @@ setCustomComponents("playground-demo", {
|
||||||
.message-body {
|
.message-body {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
// &::after {
|
||||||
content: "";
|
// content: "";
|
||||||
position: absolute;
|
// position: absolute;
|
||||||
bottom: 12px;
|
// bottom: 12px;
|
||||||
right: 12px;
|
// right: 12px;
|
||||||
width: 8px;
|
// width: 8px;
|
||||||
height: 8px;
|
// height: 8px;
|
||||||
background: #3b82f6;
|
// background: #3b82f6;
|
||||||
border-radius: 50%;
|
// border-radius: 50%;
|
||||||
animation: pulse 1.5s infinite;
|
// animation: pulse 1.5s infinite;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -441,11 +453,9 @@ setCustomComponents("playground-demo", {
|
||||||
// markstream-vue 样式覆盖
|
// markstream-vue 样式覆盖
|
||||||
.text-content {
|
.text-content {
|
||||||
:deep(p) {
|
:deep(p) {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 16px;
|
||||||
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(ul),
|
:deep(ul),
|
||||||
|
|
@ -573,7 +583,7 @@ setCustomComponents("playground-demo", {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
|
|
||||||
|
|
@ -599,7 +609,7 @@ setCustomComponents("playground-demo", {
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: white;
|
background: white;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
|
@ -635,7 +645,12 @@ setCustomComponents("playground-demo", {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
img {
|
:deep(.n-image) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-image img) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
@ -652,10 +667,11 @@ setCustomComponents("playground-demo", {
|
||||||
color: white;
|
color: white;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
img {
|
:deep(.n-image img) {
|
||||||
transform: scale(1.05);
|
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 {
|
.single-video {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
@ -734,7 +757,7 @@ setCustomComponents("playground-demo", {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: rgba(0, 0, 0, 0.03);
|
background: rgba(0, 0, 0, 0.05);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
||||||
.dark & {
|
.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 {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -852,6 +895,12 @@ setCustomComponents("playground-demo", {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 消息选择复选框
|
// 消息选择复选框
|
||||||
.message-checkbox {
|
.message-checkbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ async function textCopy(data: any) {
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="thinking-title">
|
<div class="thinking-title">
|
||||||
<!-- TODO: 深度思考样式 -->
|
<!-- TODO: 深度思考样式 -->
|
||||||
<span class="text-lg"> 深度思考</span>
|
<span > 深度思考</span>
|
||||||
<!-- 加载动画 -->
|
<!-- 加载动画 -->
|
||||||
<span v-if="node.loading" class="thinking-dots visible" aria-hidden="true">
|
<span v-if="node.loading" class="thinking-dots visible" aria-hidden="true">
|
||||||
<span class="dot dot-1" />
|
<span class="dot dot-1" />
|
||||||
|
|
@ -86,7 +86,7 @@ async function textCopy(data: any) {
|
||||||
|
|
||||||
<!-- 可折叠的内容区域 -->
|
<!-- 可折叠的内容区域 -->
|
||||||
<div class="thinking-content" :class="{ collapsed }">
|
<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" />
|
<MarkdownRender :content="node.content" @copy="textCopy" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,6 +108,7 @@ async function textCopy(data: any) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
padding: 0 0 15px 0;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -159,7 +160,7 @@ line-height: 21px;
|
||||||
.thinking-content {
|
.thinking-content {
|
||||||
color: var(--9-999999, #999);
|
color: var(--9-999999, #999);
|
||||||
font-family: "Microsoft YaHei";
|
font-family: "Microsoft YaHei";
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
|
||||||
|
|
@ -746,7 +746,7 @@ function handleClearData() {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: white;
|
background: white;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
|
@ -786,7 +786,7 @@ function handleClearData() {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ watch(show, (newVal: boolean) => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
|
|
@ -495,7 +495,7 @@ watch(show, (newVal: boolean) => {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(59, 130, 246, 0.05);
|
background: rgba(59, 130, 246, 0.05);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ watch(show, (newVal: boolean) => {
|
||||||
.share-section {
|
.share-section {
|
||||||
.share-label {
|
.share-label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
@ -282,7 +282,7 @@ watch(show, (newVal: boolean) => {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
|
|
||||||
|
|
@ -307,7 +307,7 @@ watch(show, (newVal: boolean) => {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
@ -336,7 +336,7 @@ watch(show, (newVal: boolean) => {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(245, 158, 11, 0.1);
|
background: rgba(245, 158, 11, 0.1);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ function close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip {
|
.tip {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
|
|
|
||||||
|
|
@ -583,7 +583,7 @@ onBeforeUnmount(() => {
|
||||||
|
|
||||||
.search-placeholder {
|
.search-placeholder {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-kbd {
|
.search-kbd {
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,11 @@
|
||||||
<n-tooltip :style="{ borderRadius: '5px', padding: '7px 15px' }">
|
<n-tooltip :style="{ borderRadius: '5px', padding: '7px 15px' }">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
class="action-btn"
|
class="action-btn pin-toggle-btn"
|
||||||
@click="handleTogglePin"
|
@click="handleTogglePin"
|
||||||
>
|
>
|
||||||
<!-- TODO: 取消置顶图标 -->
|
<PinOffActionIcon v-if="conversation.pinned" :size="14" />
|
||||||
<PinOff v-if="conversation.pinned" :size="14" />
|
<PinActionIcon v-else :size="14" />
|
||||||
<PinIcon v-else :size="14" />
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
{{ conversation.pinned ? '取消置顶' : '置顶' }}
|
{{ conversation.pinned ? '取消置顶' : '置顶' }}
|
||||||
|
|
@ -83,7 +82,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, nextTick } from "vue";
|
import { ref, computed, nextTick } from "vue";
|
||||||
import {
|
import {
|
||||||
PinOff,
|
|
||||||
Check,
|
Check,
|
||||||
} from "@/components/icons";
|
} from "@/components/icons";
|
||||||
import { formatTimestamp } from "@/utils/helpers";
|
import { formatTimestamp } from "@/utils/helpers";
|
||||||
|
|
@ -91,6 +89,8 @@ import type { Conversation } from "@/types/chat";
|
||||||
import { NTooltip } from "naive-ui";
|
import { NTooltip } from "naive-ui";
|
||||||
import MessageIcon from "../icons/custom/MessageIcon.vue";
|
import MessageIcon from "../icons/custom/MessageIcon.vue";
|
||||||
import PinIcon from "../icons/custom/PinIcon.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 EditIcon from "../icons/custom/EditIcon.vue";
|
||||||
import DeleteIcon from "../icons/custom/DeleteIcon.vue";
|
import DeleteIcon from "../icons/custom/DeleteIcon.vue";
|
||||||
import ShareIcon from "../icons/custom/ShareIcon.vue";
|
import ShareIcon from "../icons/custom/ShareIcon.vue";
|
||||||
|
|
@ -210,11 +210,22 @@ function handleDelete() {
|
||||||
.item-title {
|
.item-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active:hover {
|
||||||
.item-actions {
|
.item-actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
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 {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
color: #374151;
|
color: #000F33;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
|
@ -324,8 +335,23 @@ function handleDelete() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.delete:hover {
|
&.delete:hover {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
color: #f86361;
|
||||||
color: #ef4444;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-toggle-btn {
|
||||||
|
color: #666666;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #000f33;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
color: #000f33;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ function handleShareCurrent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-info {
|
.select-info {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ function handleShareCurrent() {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
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'
|
import PlusIcon from '../icons/custom/PlusIcon.vue'
|
||||||
export interface CardItem {
|
export interface CardItem {
|
||||||
id: string | number
|
id: string | number
|
||||||
|
|
@ -42,16 +42,7 @@ const isExpanded = ref(false)
|
||||||
const isWrapperHovered = ref(false)
|
const isWrapperHovered = ref(false)
|
||||||
const hoveredCardId = ref<string | number | null>(null)
|
const hoveredCardId = ref<string | number | null>(null)
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const previewCard = ref<CardItem | null>(null)
|
|
||||||
const canUpload = computed(() => props.supportsFiles || props.supportsVision)
|
const canUpload = computed(() => props.supportsFiles || props.supportsVision)
|
||||||
const isPreviewVisible = computed({
|
|
||||||
get: () => !!previewCard.value,
|
|
||||||
set: (value: boolean) => {
|
|
||||||
if (!value) {
|
|
||||||
closePreview()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const CARD_SCALE = 0.5
|
const CARD_SCALE = 0.5
|
||||||
|
|
||||||
// 默认颜色调色板 - 霓虹色系
|
// 默认颜色调色板 - 霓虹色系
|
||||||
|
|
@ -138,21 +129,6 @@ function getCardImageUrl(card: CardItem) {
|
||||||
return card.thumbnail || ''
|
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) {
|
function removeCard(card: CardItem) {
|
||||||
emit('remove', card.id)
|
emit('remove', card.id)
|
||||||
}
|
}
|
||||||
|
|
@ -170,10 +146,6 @@ function handleCardClick(card: CardItem) {
|
||||||
expandCards()
|
expandCards()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.type === 'image') {
|
|
||||||
openPreview(card)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardStyle = computed(() => (index: number, total: number) => {
|
const cardStyle = computed(() => (index: number, total: number) => {
|
||||||
|
|
@ -214,9 +186,21 @@ const cardStyle = computed(() => (index: number, total: number) => {
|
||||||
|
|
||||||
const handleDocumentClick = (event: MouseEvent) => {
|
const handleDocumentClick = (event: MouseEvent) => {
|
||||||
if (!containerRef.value) return
|
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) {
|
if (isExpanded.value) {
|
||||||
isExpanded.value = false
|
isExpanded.value = false
|
||||||
hoveredCardId.value = null
|
hoveredCardId.value = null
|
||||||
|
|
@ -262,11 +246,24 @@ watch(
|
||||||
@click="expandCards">
|
@click="expandCards">
|
||||||
<div class="cards-wrapper" @mouseenter="isWrapperHovered = true" @mouseleave="isWrapperHovered = false">
|
<div class="cards-wrapper" @mouseenter="isWrapperHovered = true" @mouseleave="isWrapperHovered = false">
|
||||||
<TransitionGroup name="card-spread">
|
<TransitionGroup name="card-spread">
|
||||||
<div v-for="(card, index) in cards" :key="card.id" class="card" :style="cardStyle(index, cards.length)"
|
<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)">
|
@mouseenter="hoveredCardId = card.id" @mouseleave="hoveredCardId = null" @click="handleCardClick(card)">
|
||||||
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
|
<div class="card-glow" :style="{ background: getCardColor(card, index) }" />
|
||||||
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
|
<div v-if="card.type === 'image' && getCardImageUrl(card)" class="card-media">
|
||||||
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
|
<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 v-if="card.uploading" class="card-media-uploading">
|
||||||
<div class="card-media-uploading-spinner" aria-hidden="true" />
|
<div class="card-media-uploading-spinner" aria-hidden="true" />
|
||||||
<span class="card-media-uploading-text">上传中</span>
|
<span class="card-media-uploading-text">上传中</span>
|
||||||
|
|
@ -276,9 +273,15 @@ watch(
|
||||||
<span class="card-media-deleting-text">删除中</span>
|
<span class="card-media-deleting-text">删除中</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="card-preview">
|
<div v-else-if="card.type === 'image'" class="card-preview">
|
||||||
<div v-if="getCardImageUrl(card)" class="card-preview-image">
|
<div v-if="getCardImageUrl(card)" class="card-preview-image">
|
||||||
<img :src="getCardImageUrl(card)" :alt="getCardTitle(card, index)" />
|
<NImage
|
||||||
|
:src="getCardImageUrl(card)"
|
||||||
|
:alt="getCardTitle(card, index)"
|
||||||
|
object-fit="cover"
|
||||||
|
:preview-disabled="!isExpanded || card.uploading || card.deleting"
|
||||||
|
:img-props="{ loading: 'lazy' }"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-preview-fallback">
|
<div class="card-preview-fallback">
|
||||||
<span class="card-icon">
|
<span class="card-icon">
|
||||||
|
|
@ -290,30 +293,49 @@ watch(
|
||||||
<span class="card-preview-deleting-text">删除中</span>
|
<span class="card-preview-deleting-text">删除中</span>
|
||||||
</div>
|
</div>
|
||||||
</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))"
|
<button v-if="!card.deleting && isWrapperHovered && (hoveredCardId === card.id || (!hoveredCardId && index === 0))"
|
||||||
type="button" class="card-delete-btn" title="删除 OSS 文件" @click.stop="removeCard(card)">
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ getCardTitle(card, index) }}
|
||||||
|
</NTooltip>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NModal v-model:show="isPreviewVisible" preset="card" :mask-closable="true" :close-on-esc="true"
|
<button
|
||||||
:on-close="removePreviewCard" class="image-preview-modal"
|
v-if="!isExpanded"
|
||||||
:content-style="{ width: '100%', height: '100%', padding: '0', overflow: 'hidden' }"
|
type="button"
|
||||||
:style="{ width: '100vw', height: '100vh', maxWidth: '100vw', margin: '0', padding: '0', borderRadius: '0', background: 'rgba(5, 7, 12, 0.96)' }">
|
class="cards-upload-fab"
|
||||||
<div class="image-preview-shell">
|
:class="{ disabled: !canUpload }"
|
||||||
<NImage v-if="previewCard" :src="getCardImageUrl(previewCard)" :alt="getCardTitle(previewCard, 0)"
|
:disabled="!canUpload"
|
||||||
object-fit="contain" preview-disabled :img-props="{
|
:title="canUpload ? '上传附件或图片' : '当前模型不支持上传'"
|
||||||
style: {
|
@click.stop="emit('add-upload')"
|
||||||
width: '100%',
|
>
|
||||||
height: '100%',
|
<PlusIcon :size="12" />
|
||||||
maxWidth: '100%',
|
</button>
|
||||||
maxHeight: '100%',
|
|
||||||
},
|
|
||||||
}" class="image-preview-content" />
|
|
||||||
</div>
|
|
||||||
</NModal>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -329,6 +351,35 @@ watch(
|
||||||
perspective: 1000px;
|
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 {
|
.stacked-cards-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -348,17 +399,18 @@ watch(
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 74%;
|
width: 74%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 5px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
|
border: 1px solid var(--card-border-color, var(--ffffff, #FFF));
|
||||||
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
background: url(<path-to-image>) lightgray 50% / cover no-repeat;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
/* overflow: hidden; */
|
||||||
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
|
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media,
|
.card-media,
|
||||||
.card-preview {
|
.card-preview,
|
||||||
|
.card-file-preview {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
@ -366,6 +418,8 @@ watch(
|
||||||
|
|
||||||
.card-media {
|
.card-media {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media-uploading {
|
.card-media-uploading {
|
||||||
|
|
@ -456,33 +510,41 @@ watch(
|
||||||
|
|
||||||
.card-delete-btn {
|
.card-delete-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.35rem;
|
top: -0.3rem;
|
||||||
right: 0.35rem;
|
right: -0.3rem;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 0.95rem;
|
width: 16.001px;
|
||||||
height: 0.95rem;
|
height: 16px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 50%;
|
||||||
background: rgba(239, 68, 68, 0.9);
|
background: transparent;
|
||||||
color: #fff;
|
|
||||||
font-size: 0.55rem;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
backdrop-filter: blur(10px);
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-delete-btn:hover {
|
.card-delete-btn:hover {
|
||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
background: rgba(220, 38, 38, 0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-media img,
|
.card-delete-btn svg {
|
||||||
.card-preview-image img {
|
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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
@ -555,6 +617,38 @@ watch(
|
||||||
font-size: 0.95rem;
|
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 {
|
.card:hover .card-glow {
|
||||||
opacity: 0.15;
|
opacity: 0.15;
|
||||||
|
|
@ -709,61 +803,6 @@ watch(
|
||||||
transform: translateX(-50%) translateY(6px);
|
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) {
|
@media (max-width: 640px) {
|
||||||
.stacked-cards-container {
|
.stacked-cards-container {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
--header-height: 60px;
|
--header-height: 60px;
|
||||||
--app-text-color: #333;
|
--app-text-color: #333;
|
||||||
--app-font-family: "Microsoft YaHei", sans-serif;
|
--app-font-family: "Microsoft YaHei", sans-serif;
|
||||||
--app-font-size: 12px;
|
--app-font-size: 14px;
|
||||||
--app-font-style: normal;
|
--app-font-style: normal;
|
||||||
--app-font-weight: 400;
|
--app-font-weight: 400;
|
||||||
--app-line-height: normal;
|
--app-line-height: normal;
|
||||||
|
|
@ -91,3 +91,7 @@ body,
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateX(-20px);
|
transform: translateX(-20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr.hr-node[custom-id="playground-demo"]{
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ authStore.init()
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -347,7 +347,7 @@ onMounted(() => {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
|
|
||||||
&.expired {
|
&.expired {
|
||||||
|
|
@ -411,7 +411,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.verify-error {
|
.verify-error {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -482,7 +482,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-count {
|
.conversation-count {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue