diff --git a/docs/experience.md b/docs/experience.md index 9c226519..411d07b0 100644 --- a/docs/experience.md +++ b/docs/experience.md @@ -339,3 +339,45 @@ if (updatedTemplate && contentChanged) { --- **记住**:好的经验文档应该能让团队成员快速找到解决方案,而不是重新踩坑。 + +## 7. UI 主题系统与第三方库样式冲突处理 + +**问题场景**: 在开发多主题功能(特别是紫色、绿色等自定义深色主题)时,发现集成了 Tailwind Typography (`prose`) 插件的 Markdown 渲染组件,其背景和文本颜色无法正确应用主题色,而是被覆盖为不协调的亮色样式(如白色背景)。 + +**根本原因分析**: + +问题的核心在于项目自定义的、基于 `data-theme` 属性的颜色主题系统,与 Tailwind Typography (`prose`) 插件预设的、自成体系的颜色方案发生了直接冲突。 + +1. **`prose` 的强主张**: `@tailwindcss/typography` 插件不仅仅是一个布局工具,它会为 HTML 内容注入一套完整的视觉方案,其中**包含了固定的颜色、字体、背景等样式**。 +2. **默认亮色偏好**: `prose` 的默认配置(如 `prose-stone`)是为亮色背景设计的,它会强制设定深色的文本颜色。 +3. **`dark:` 模式的局限性**: `prose` 的颜色反转机制 (`dark:prose-invert`) 强依赖于 `` 标签上的 `dark` 类。我们自定义的深色主题(如 `data-theme="purple"`)虽然视觉上是深色的,但并未触发 Tailwind 的 `dark` 模式,因此 `prose` 依然应用其默认的亮色样式,导致了颜色覆盖。 + +**解决方案与最佳实践**: + +面对这种强样式主张的第三方库,必须采取**彻底隔离**的策略,不能试图"混合"使用。 + +1. **禁止部分应用**: 实践证明,试图通过 `@apply prose-sm` 等方式只"借用" `prose` 的布局功能是行不通的。这依然会引入我们不希望的颜色样式,导致不可预测的覆盖问题。 + +2. **手动重建布局**: 最稳健的解决方案是,在需要应用自定义主题的组件中,**完全移除** `@apply prose` 或其任何变体。然后,参考 `prose` 的文档或默认样式,**手动为各个 Markdown 元素 (`h1`, `p`, `ul` 等) 添加纯粹的、不包含颜色的布局和间距样式**。 + +3. **控制权归还**: 通过手动重建布局,我们将样式的控制权完全收归到自己的主题系统中。这样,我们在各个主题下为元素定义的颜色、背景、边框等样式才能不受干扰地、正确地应用。 + +**示例 - 手动重建的 Markdown 布局**: + +```css +/* 在全局 theme.css 中定义,不属于任何特定主题 */ +.theme-markdown-content { + @apply max-w-none; +} + +.theme-markdown-content > :first-child { @apply mt-0; } +.theme-markdown-content > :last-child { @apply mb-0; } +.theme-markdown-content h1 { @apply text-2xl font-bold my-4; } +.theme-markdown-content h2 { @apply text-xl font-semibold my-3; } +.theme-markdown-content p { @apply my-3 leading-relaxed; } +.theme-markdown-content ul, +.theme-markdown-content ol { @apply my-3 pl-6 space-y-2; } +.theme-markdown-content pre { @apply my-4 p-4 rounded-lg text-sm; } +/* ... etc ... */ +``` +通过这种方式,我们既保留了优美的排版,又确保了自定义主题的颜色能够正确渲染。 diff --git a/docs/output-display-component-design.md b/docs/output-display-component-design.md deleted file mode 100644 index 3b7d7313..00000000 --- a/docs/output-display-component-design.md +++ /dev/null @@ -1,203 +0,0 @@ -# OutputDisplay 组件体系设计文档 - -## 1. 概述 - -**OutputDisplay** 是一个通用的、模块化的内容输出组件体系,旨在统一管理系统中所有内容输出区域的显示和交互。它由一组分工明确的组件构成,共同解决样式不统一、交互分散、维护困难等问题。 - -### 组件体系构成 - -- **`OutputDisplay` (外观组件)**:作为整个体系的统一入口,供外部调用。它封装了核心显示逻辑和全屏功能,并向上传递事件。 -- **`OutputDisplayCore` (核心引擎)**:实现所有核心的显示逻辑,包括只读/编辑/对比模式切换、推理内容管理、工具栏交互等。 -- **`OutputDisplayFullscreen` (全屏封装)**:一个轻量级封装,结合 `FullscreenDialog` 和 `OutputDisplayCore` 提供沉浸式的全屏查看体验。 - -## 2. 设计目标 - -- **统一性**:提供一致的UI和交互体验。 -- **复用性**:一个组件体系适配多种使用场景。 -- **可维护性**:集中管理核心逻辑,降低耦合度。 -- **扩展性**:通过配置支持不同功能组合。 - -## 3. 组件接口设计 (`OutputDisplayCore`) - -核心引擎 `OutputDisplayCore` 是功能的主要提供者。 - -### Props 属性 - -```typescript -type ActionName = 'fullscreen' | 'diff' | 'copy' | 'edit' | 'reasoning'; - -interface OutputDisplayCoreProps { - // === 内容相关 === - content?: string; // 主要内容(或对比模式下的新内容) - originalContent?: string; // 对比模式下的原始(旧)内容。**这是启用内置对比(diff)模式的前提** - reasoning?: string; // 推理内容 - - // === 显示模式 === - mode: 'readonly' | 'editable'; // 'readonly': 渲染Markdown; 'editable': 提供可编辑的textarea - reasoningMode: 'show' | 'hide' | 'auto'; // 推理内容显示模式: 'show'-常驻, 'hide'-隐藏, 'auto'-有内容时自动显示 - - // === 功能开关 === - // 通过此数组控制悬浮工具栏中显示哪些功能按钮。 - // 例如,当数组中包含 'diff' 且 `originalContent` prop 也被提供时,对比按钮才会出现。 - enabledActions?: ActionName[]; // e.g., ['copy', 'fullscreen', 'edit'] - - // === 样式配置 === - height?: string | number; // 内容区域高度, e.g., '100%' or 500 - placeholder?: string; // 内容为空时的占位符文本 - - // === 状态 === - loading?: boolean; // 加载状态,显示遮罩 - streaming?: boolean; // 流式输出状态,显示流式指示器 -} -``` - -### Events 事件 - -```typescript -interface OutputDisplayCoreEvents { - 'update:content': (content: string) => void; // 在编辑模式下,内容发生变更时触发 - 'update:reasoning': (reasoning: string) => void; // 推理内容更新时触发 - 'copy': (content: string, type: 'content' | 'reasoning' | 'all') => void; // 点击复制按钮时触发 - 'fullscreen': () => void; // 点击全屏按钮时触发 - 'edit-start': () => void; // 进入编辑模式时触发 - 'edit-end': () => void; // 退出编辑模式时触发 - 'reasoning-toggle': (expanded: boolean) => void; // 推理内容区域展开/折叠时触发 - 'view-change': (mode: 'base' | 'diff') => void; // 在内置的常规视图和对比视图之间切换时触发 -} -``` - -## 4. 组件结构设计 - -### 4.1. 组件体系关系 - -`OutputDisplay` 外观组件是实际应用中应直接使用的组件。它内部管理 `OutputDisplayCore` 和 `OutputDisplayFullscreen` 的实例化和通信。 - -```mermaid -graph TD - A[应用 App.vue] --> B(OutputDisplay); - B -- "实例化并传递Props" --> C(OutputDisplayCore); - B -- "监听 'fullscreen' 事件" --> D{显示/隐藏}; - D --> E(OutputDisplayFullscreen); - E -- "内部实例化" --> C; - C -- "通过 emit 触发事件" --> B; - B -- "继续向上传递事件" --> A; - - subgraph "组件体系" - B; C; D; E; - end -``` - -### 4.2. `OutputDisplayCore` 内部结构 - -核心组件根据不同的模式和状态,动态渲染其内部视图。 - -``` -OutputDisplayCore -├── FloatingToolbar (悬浮工具栏, v-show="isHovering") -│ ├── ReasoningToggle (推理过程按钮) -│ ├── ViewModeToggle (文本/视图切换) -│ ├── DiffButton (对比按钮) -│ ├── CopyButton (复制按钮) -│ └── FullscreenButton (全屏按钮) -├── ReasoningSection (v-if="shouldShowReasoning") -│ └── MarkdownRenderer (渲染推理内容) -└── MainContent (flex: 1) - ├── TextDiffUI (v-if="internalViewMode === 'diff'") - ├── ReadonlyView (v-else-if="mode === 'readonly'") - │ └── MarkdownRenderer (渲染主要内容) - └── EditableView (v-else) - ├── textarea (v-if="isEditing") - └── MarkdownRenderer (v-else, 点击可编辑) -``` - -## 5. 功能特性 - -### 5.1. 推理内容管理 (`reasoning`) -- **智能显示**: 根据 `reasoningMode` 自动判断是否显示。 -- **流式指示**: 在 `streaming` 状态下,若只有推理内容,会显示"生成中..."指示器。 -- **独立复制**: 推理内容有独立的复制按钮。 - -### 5.2. 多视图模式 -- **只读 (`readonly`)**: 使用 `MarkdownRenderer` 展示内容,支持代码高亮等富文本格式。 -- **可编辑 (`editable`)**: - - 默认显示渲染后的 `Markdown`。 - - 点击内容区域或切换按钮可进入 `textarea` 编辑模式。 - - 编辑内容通过 `update:content` 事件实时同步。 -- **内置对比 (`diff`)**: - - **激活机制**: 此模式并非由独立的prop控制,而是通过组合条件隐式激活。当`originalContent` prop被提供,且`enabledActions`数组包含`'diff'`时,用户界面上才会出现进入对比模式的按钮。 - - **功能**: 激活后,组件内部使用`TextDiffUI`来展示`content`与`originalContent`之间的差异。 - -### 5.3. 悬浮工具栏 -- **按需显示**: `enabledActions` prop 控制显示哪些功能按钮。 -- **情景可见**: 仅在鼠标悬浮在组件上时显示,保持界面整洁。 -- **独立控制**: 每个按钮的可见性都与相关状态(如是否有内容可复制)绑定。 - -## 6. 使用场景与模式 - -### 6.1. 场景:优化结果展示与编辑 - -这是最完整的用例,展示了可编辑、可对比、带推理过程的优化结果。 - -```vue - -``` - -### 6.2. 场景:只读的测试结果 - -用于展示流式生成的测试结果,不可编辑。 - -```vue - -``` - -### 6.3. 模式:应用级对比布局 - -要并排对比两个不同的结果(例如,原始Prompt测试结果 vs 优化后Prompt测试结果),应使用**两个独立**的 `OutputDisplay` 组件。这并非组件的内置功能,而是一种应用层布局模式。 - -```vue -
- - - - - -
-``` -**优势**: 每个结果区域都是一个功能完整的组件,可以独立进行复制、全屏等操作。 - -## 7. 技术实现要点 - -- **状态驱动UI**: 组件的所有显示和交互都由Props和内部`ref`状态驱动,确保UI的可预测性。 -- **逻辑封装**: `OutputDisplayCore` 封装了所有复杂的显示和交互逻辑,`OutputDisplay` 负责组合与门面,职责清晰。 -- **实时同步**: 在编辑模式下,通过 `watch` 实时 `emit` 内容更新,确保了父组件状态的即时性,避免了数据不一致问题。 - -## 8. 迁移策略 - -项目已完成从旧有分散组件到 `OutputDisplay` 体系的迁移。所有内容输出区域(如 `PromptPanel`, `TestPanel`)均已使用此组件,实现了UI和交互的统一。未来的新功能也应基于此组件体系进行扩展。 \ No newline at end of file diff --git a/docs/output-display-v2-design.md b/docs/output-display-v2-design.md new file mode 100644 index 00000000..3b1d17c9 --- /dev/null +++ b/docs/output-display-v2-design.md @@ -0,0 +1,238 @@ +# OutputDisplay V2 设计文档 + +## 1. 核心设计理念 + +V2版本的核心目标是解决V1版本中功能控件布局混淆、作用域不明确的问题。新的设计遵循以下核心原则: + +- **控件分组 (Control Grouping)**: 功能相似或作用域相同的控件应在视觉上归为一组。 +- **作用域关联 (Scope Association)**: 控件的布局位置应直观地反映其控制的UI区域。 +- **高可见性 (High Visibility)**: 常用功能应始终可见且易于访问,避免不必要的悬停操作。 + +## 2. 最终布局方案 (V3) + +经过多轮讨论,最终确定V3方案,其核心是创建一个统一的、始终可见的顶层工具栏,并通过内部分组实现逻辑分离与视觉和谐。 + +### 2.1 可视化布局 + +``` ++----------------------------------------------------------------------+ +| [渲染|原文|对比] (左侧固定) [复制][全屏*] (右侧固定) | <-- 统一顶层工具栏 (始终可见) ++----------------------------------------------------------------------+ +| | +| [思考过程]..........................................[展开/折叠] (固定) | <-- 思考过程面板 ++----------------------------------------------------------------------+ +| (思考过程内容区, 可选,可折叠) | +| (内部可带自己的复制按钮) | ++----------------------------------------------------------------------+ +| | +| (主要内容区) | +| | ++----------------------------------------------------------------------+ + +* 全屏按钮在全屏视图下隐藏 +``` + +### 2.2 控件详解 + +#### 2.2.1 顶层工具栏 (Primary Toolbar) + +- **定位与可见性**: 固定在整个组件最顶部,始终可见。 +- **功能**: 作为所有主要操作的统一入口。 +- **内部分组**: + - **左侧组 (视图控制)**: + - **成员**: `渲染(Render)`, `原文(Source)`, `对比(Diff)` 按钮组。 + - **作用**: 控制下方"主要内容区域"的呈现方式。 + - **右侧组 (动作执行)**: + - **成员**: `复制(Copy)`, `全屏(Fullscreen)` 按钮。 + - **作用**: 对内容或组件执行单次动作。`复制`按钮作用于"主要内容",`全屏`按钮作用于整个组件。 + - **特殊规则**: `全屏`按钮在组件已处于全屏模式时(如在 `OutputDisplayFullscreen.vue` 中使用时)应被隐藏。 + +#### 2.2.2 "思考过程"面板 (Reasoning Panel) + +- **定位**: 位于顶层工具栏下方,主要内容区域上方。 +- **结构**: 自包含模块,拥有独立的标题栏和内容区。 +- **控件**: + - **展开/折叠**: 位于标题栏右侧,控制内容区的显隐。整个标题栏都可点击触发。 + - **复制思考过程**: 为确保作用域清晰,此按钮可放置在内容区内部(例如右下角),仅在内容区展开时可见。 + +## 3. 组件接口设计 (`OutputDisplayCore`) + +V2 版本的外部接口(Props & Events)与 V1 版本保持高度兼容,核心变化体现在内部实现和用户体验上。 + +### Props 属性 + +```typescript +type ActionName = 'fullscreen' | 'diff' | 'copy' | 'edit' | 'reasoning'; + +interface OutputDisplayCoreProps { + // ... 其他 props 保持不变 ... + content?: string; + originalContent?: string; // 依然是激活"对比模式"按钮的先决条件 + reasoning?: string; + mode: 'readonly' | 'editable'; // 定义组件的"能力",决定在原文模式下是否可编辑 + enabledActions?: ActionName[]; // 依然用于控制工具栏功能 + // ... +} +``` + +## 4. 数据流与状态管理 (草稿内容处理) + +一个常见的问题是:用户在原文模式下编辑的内容(可视为"草稿")是如何被管理的? + +**核心原则**:`OutputDisplay` 是一个纯粹的 **受控组件 (Controlled Component)**。它自身不持有任何临时的"草稿"状态。它的职责是忠实地展示父组件通过 `props` 传入的数据,并通过 `events` 将用户的输入行为通知给父组件。 + +这种模式遵循了 **单一数据源 (Single Source of Truth)** 的架构原则,确保了数据流的可预测性和一致性。 + +### 数据流闭环 + +```mermaid +graph TD + subgraph Parent Component (e.g., PromptPanel) + A(State: optimizedPrompt) + end + + subgraph OutputDisplay + B(Textarea) + end + + A -- "1. 状态下发 (Props)" --> B; + B -- "2. 用户输入触发 @input 事件" --> C{emit('update:content', ...)} + C -- "3. 变更请求 (Events)" --> A; + A -- "4. 视图自动同步 (Re-render)" --> B; +``` + +**工作流程解析**: +1. **状态下发**:父组件将 `optimizedPrompt` 状态通过 `:content` prop 传递给 `OutputDisplay`。 +2. **变更请求**:当用户在 ` + + + +
-
+
{{ placeholder || t('common.loading') }}
-
+
{{ placeholder || t('common.noContent') }}
- - -
- - - - -
- -
- {{ placeholder || t('common.clickToEdit') }} -
-
-
@@ -251,23 +204,19 @@ const emit = defineEmits<{ }>() // 内部状态 -const isEditing = ref(false) -const editingContent = ref('') const isReasoningExpanded = ref(false) -const editTextarea = ref(null) -const isHovering = ref(false) -const manualToggleActive = ref(false) -const viewStateBeforeDiff = ref<{ isEditing: boolean, manualToggleActive: boolean } | null>(null) - -// 推理区域自动滚动 const reasoningContentRef = ref(null) -// 对比模式状态 -const internalViewMode = ref<'base' | 'diff'>('base') +// 新的视图状态机 +const internalViewMode = ref<'render' | 'source' | 'diff'>('render') const compareResult = ref() const isActionEnabled = (action: ActionName) => props.enabledActions.includes(action) +const hasToolbar = computed(() => + ['diff', 'copy', 'fullscreen', 'edit'].some(action => isActionEnabled(action as ActionName)) +) + // 计算属性 const displayContent = computed(() => (props.content || '').trim()) const displayReasoning = computed(() => (props.reasoning || '').trim()) @@ -286,22 +235,11 @@ const shouldShowReasoning = computed(() => { return hasReasoning.value || props.streaming }) -const hasActions = computed(() => { - const reasoningButtonVisible = isActionEnabled('reasoning') && shouldShowReasoning.value - const viewToggleButtonVisible = props.mode === 'editable' && isActionEnabled('edit') && !props.streaming - const diffButtonVisible = isActionEnabled('diff') && !!props.originalContent && !!props.content - const copyButtonVisible = isActionEnabled('copy') && hasContent.value - const fullscreenButtonVisible = isActionEnabled('fullscreen') && hasContent.value - - return reasoningButtonVisible || viewToggleButtonVisible || diffButtonVisible || copyButtonVisible || fullscreenButtonVisible -}) - const isEmpty = computed(() => !hasContent.value && !props.loading && !props.streaming) const displayClasses = computed(() => ({ 'output-display-core--loading': props.loading, - 'output-display-core--streaming': props.streaming, - 'output-display-core--editable': props.mode === 'editable' && isEditing.value + 'output-display-core--streaming': props.streaming })) const computedHeight = computed(() => { @@ -311,6 +249,12 @@ const computedHeight = computed(() => { return props.height }) +// 处理原文模式输入 +const handleSourceInput = (event: Event) => { + const target = event.target as HTMLTextAreaElement + emit('update:content', target.value) +} + // 复制功能 const handleCopy = (type: 'content' | 'reasoning' | 'all') => { let textToCopy = '' @@ -342,60 +286,6 @@ const handleFullscreen = () => { emit('fullscreen') } -// 编辑功能 -const handleManualViewToggle = () => { - manualToggleActive.value = true - if (isEditing.value) { - stopEditing() - } else { - startEditing() - } -} - -const handleContainerClick = () => { - if (manualToggleActive.value) return - startEditing() -} - -const startEditing = () => { - if (props.streaming) return // 流式传输期间禁止编辑 - if (!isActionEnabled('edit') || props.mode !== 'editable' || internalViewMode.value === 'diff') return - - editingContent.value = props.content || '' - isEditing.value = true - emit('edit-start') - - nextTick(() => { - editTextarea.value?.focus() - }) -} - -const stopEditing = () => { - if (!isEditing.value) return - - if (editingContent.value !== props.content) { - emit('update:content', editingContent.value) - } - - isEditing.value = false - emit('edit-end') -} - -const handleTextareaBlur = () => { - if (manualToggleActive.value) return - - setTimeout(() => { - if (isEditing.value) { - stopEditing() - } - }, 200) -} - -const handleInput = (event: Event) => { - const target = event.target as HTMLTextAreaElement - editingContent.value = target.value -} - // 推理内容 const toggleReasoning = () => { isReasoningExpanded.value = !isReasoningExpanded.value @@ -413,31 +303,6 @@ const scrollReasoningToBottom = () => { } // 对比功能 -const toggleDiffView = () => { - const isEnteringDiff = internalViewMode.value === 'base' - - if (isEnteringDiff) { - viewStateBeforeDiff.value = { - isEditing: isEditing.value, - manualToggleActive: manualToggleActive.value, - } - if (isEditing.value) { - stopEditing() - } - internalViewMode.value = 'diff' - } else { // Exiting diff - internalViewMode.value = 'base' - if (viewStateBeforeDiff.value) { - manualToggleActive.value = viewStateBeforeDiff.value.manualToggleActive - if (viewStateBeforeDiff.value.isEditing) { - startEditing() - } - viewStateBeforeDiff.value = null - } - } - emit('view-change', internalViewMode.value) -} - const updateCompareResult = async () => { if (internalViewMode.value === 'diff' && props.originalContent && props.content) { try { @@ -453,6 +318,25 @@ const updateCompareResult = async () => { } } +// 智能自动切换逻辑 +const previousViewMode = ref<'render' | 'source' | 'diff' | null>(null) + +watch(() => props.streaming, (isStreaming) => { + if (isStreaming) { + // 记住当前模式,并强制切换到原文模式 + if (internalViewMode.value !== 'source') { + previousViewMode.value = internalViewMode.value + internalViewMode.value = 'source' + } + } else { + // 流式结束后,恢复之前的模式 + if (previousViewMode.value) { + internalViewMode.value = previousViewMode.value + previousViewMode.value = null + } + } +}) + watch(internalViewMode, updateCompareResult, { immediate: true }) watch(() => [props.content, props.originalContent], () => { if (internalViewMode.value === 'diff') { @@ -460,27 +344,6 @@ watch(() => [props.content, props.originalContent], () => { } }) -// 监听编辑内容变化,实时同步到父组件 -watch(editingContent, (newContent) => { - // 只有在编辑状态下才触发更新 - if (isEditing.value && newContent !== props.content) { - emit('update:content', newContent) - } -}, { immediate: false }) - -// 监听props.content变化,同步到编辑内容 -watch(() => props.content, (newContent, oldContent) => { - if (!isEditing.value) { - editingContent.value = newContent || '' - } -}) - -watch(() => props.streaming, (newStreaming, oldStreaming) => { - if (newStreaming && !oldStreaming && isEditing.value) { - stopEditing() - } -}) - watch(() => props.reasoning, (newReasoning, oldReasoning) => { // 当推理内容从无到有,且没有主要内容时,自动展开思考过程 if (!oldReasoning && newReasoning && !hasContent.value) { @@ -499,73 +362,43 @@ const resetReasoningState = (initialState: boolean) => { isReasoningExpanded.value = initialState } -// 强制刷新内容 - 用于版本切换时确保内容同步 -const forceRefreshContent = () => { - // 如果正在编辑,强制停止编辑 - if (isEditing.value) { - isEditing.value = false - emit('edit-end') - } - - // 强制同步 props.content 到内部状态 - editingContent.value = props.content || '' - - // 重置手动切换状态 - manualToggleActive.value = false +// 强制退出编辑状态 - 重构为强制切换到渲染模式 +const forceExitEditing = () => { + internalViewMode.value = 'render' } -// 强制退出编辑状态 - 用于开始优化/迭代前 -const forceExitEditing = () => { - if (isEditing.value) { - // 保存当前编辑的内容 - if (editingContent.value !== props.content) { - emit('update:content', editingContent.value) - } - - isEditing.value = false - emit('edit-end') - } +// 保持向后兼容的方法 +const forceRefreshContent = () => { + // V2版本中这个方法不再需要,但保留以确保向后兼容 } +const themeToolbarBg = 'theme-toolbar-bg' +const themeToolbarBorder = 'theme-toolbar-border' +const themeToolbarButton = 'theme-toolbar-button' +const themeToolbarButtonActive = 'theme-toolbar-button-active' + defineExpose({ resetReasoningState, forceRefreshContent, forceExitEditing }) \ No newline at end of file diff --git a/packages/ui/src/components/OutputDisplayFullscreen.vue b/packages/ui/src/components/OutputDisplayFullscreen.vue index 0552b1f1..1d692e62 100644 --- a/packages/ui/src/components/OutputDisplayFullscreen.vue +++ b/packages/ui/src/components/OutputDisplayFullscreen.vue @@ -2,7 +2,6 @@ { emit('copy', content, type) } - - - \ No newline at end of file + \ No newline at end of file diff --git a/packages/ui/src/components/OutputPanel.vue b/packages/ui/src/components/OutputPanel.vue index 624aec20..94bbee3a 100644 --- a/packages/ui/src/components/OutputPanel.vue +++ b/packages/ui/src/components/OutputPanel.vue @@ -64,7 +64,7 @@