Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,11 @@ onBeforeUnmount(() => {

<!-- Main content area -->
<div
class="flex-1 min-w-0 bg-background overflow-hidden rounded-tl-xl border-black/20 dark:border-white/10 border-l border-t"
class="flex h-full min-h-0 flex-1 min-w-0 flex-col overflow-hidden rounded-tl-xl border-l border-t border-black/20 bg-background dark:border-white/10"
>
<RouterView v-if="isStartupRouteReady" />
<div class="min-h-0 flex-1">
<RouterView v-if="isStartupRouteReady" />
</div>
</div>
</div>
</div>
Expand Down
49 changes: 31 additions & 18 deletions src/renderer/src/components/artifacts/HTMLArtifact.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
<template>
<div class="w-full h-full overflow-auto">
<div
class="relative w-full h-full"
:class="viewportSize !== 'desktop' ? 'flex items-center justify-center' : ''"
>
<div :class="viewportSize === 'desktop' ? 'relative w-full h-full' : 'relative'">
<iframe
ref="iframeRef"
:srcdoc="block.content"
:class="viewportClasses"
:style="viewportStyles"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
<div :class="containerClasses" data-testid="html-artifact-root">
<div :class="frameContainerClasses">
<iframe
ref="iframeRef"
:srcdoc="block.content"
:class="viewportClasses"
:style="viewportStyles"
sandbox="allow-scripts allow-same-origin"
data-testid="html-artifact-iframe"
></iframe>
</div>
</div>
</template>
Expand All @@ -39,22 +35,39 @@ const props = defineProps<{
}>()

const iframeRef = ref<HTMLIFrameElement>()
const resolvedViewportSize = computed(() => props.viewportSize || 'desktop')

const containerClasses = computed(() => {
if (resolvedViewportSize.value === 'desktop') {
return 'flex h-full min-h-0 w-full overflow-hidden'
}

return 'flex h-full min-h-0 w-full items-center justify-center overflow-auto'
})

const frameContainerClasses = computed(() => {
if (resolvedViewportSize.value === 'desktop') {
return 'h-full min-h-0 w-full'
}

return 'relative shrink-0'
})

const viewportClasses = computed(() => {
const size = props.viewportSize || 'desktop'
const size = resolvedViewportSize.value
const baseClasses = 'html-iframe-wrapper transition-all duration-300 ease-in-out'

switch (size) {
case 'mobile':
case 'tablet':
return `${baseClasses} border border-gray-300 dark:border-gray-600 relative`
default:
return `${baseClasses} w-full h-full`
return `${baseClasses} block h-full min-h-0 w-full`
}
})

const viewportStyles = computed(() => {
const size = props.viewportSize || 'desktop'
const size = resolvedViewportSize.value

if (size === 'mobile' || size === 'tablet') {
const dimensions = VIEWPORT_SIZES[size]
Expand All @@ -75,7 +88,7 @@ const setupIframe = () => {
if (!doc) return

// Add viewport meta tag
const viewportSize = props.viewportSize || 'desktop'
const viewportSize = resolvedViewportSize.value
let viewportContent = 'width=device-width, initial-scale=1.0'

if (viewportSize === 'mobile' || viewportSize === 'tablet') {
Expand Down
22 changes: 13 additions & 9 deletions src/renderer/src/components/artifacts/MermaidArtifact.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<template>
<div class="w-full h-full flex flex-col overflow-hidden">
<div
class="flex h-full min-h-0 w-full flex-col overflow-hidden"
data-testid="mermaid-artifact-root"
>
<div
v-if="props.isPreview"
ref="mermaidRef"
class="w-full h-full p-4 overflow-auto flex items-center justify-center [&_svg]:!w-full [&_svg]:!h-full [&_svg]:max-h-[calc(100vh-120px)] [&_svg]:object-contain"
class="flex h-full min-h-0 w-full flex-1 items-center justify-center overflow-auto p-4 [&_svg]:max-h-full [&_svg]:max-w-full [&_svg]:h-auto [&_svg]:w-auto"
data-testid="mermaid-artifact-preview"
></div>
<div v-else class="h-full p-4">
<div v-else class="h-full min-h-0 p-4">
<pre
class="rounded-lg bg-muted p-4 h-full m-0 overflow-auto"
class="m-0 h-full min-h-0 overflow-auto rounded-lg bg-muted p-4"
><code class="font-mono text-sm leading-6 h-full block">{{ props.block.content }}</code></pre>
</div>
</div>
Expand Down Expand Up @@ -52,17 +56,17 @@ const sanitizeMermaidContent = (content: string): string => {
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
// Object 和 Embed 标签 - 可以执行代码
/<object[^>]*>[\s\S]*?<\/object>/gi,
/<embed[^>]*[^>]*>/gi,
/<embed\b(?:"[^"]*"|'[^']*'|[^'">])*?>/gi,
// Form 标签 - 可能用于 CSRF
/<form[^>]*>[\s\S]*?<\/form>/gi,
// Link 标签 - 可能加载恶意样式或脚本
/<link[^>]*>/gi,
/<link\b(?:"[^"]*"|'[^']*'|[^'">])*?>/gi,
// Style 标签 - 可能包含恶意 CSS
/<style[^>]*>[\s\S]*?<\/style>/gi,
// Meta 标签 - 可能用于重定向或执行
/<meta[^>]*>/gi,
/<meta\b(?:"[^"]*"|'[^']*'|[^'">])*?>/gi,
// Img 标签 - PoC 中使用的攻击向量,带事件处理器特别危险
/<img[^>]*>/gi
/<img\b(?:"[^"]*"|'[^']*'|[^'">])*?>/gi
]

// 移除危险标签
Expand All @@ -72,7 +76,7 @@ const sanitizeMermaidContent = (content: string): string => {

// 移除所有事件处理器属性 (on* 属性)
// 这包括 onerror, onclick, onload, onmouseover 等
sanitized = sanitized.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
sanitized = sanitized.replace(/on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')

// 移除危险的协议
const dangerousProtocols = [/javascript\s*:/gi, /vbscript\s*:/gi, /data\s*:\s*text\/html/gi]
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/src/components/artifacts/ReactArtifact.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<template>
<div class="w-full h-full overflow-auto">
<div class="flex h-full min-h-0 w-full overflow-hidden" data-testid="react-artifact-root">
<iframe
ref="iframeRef"
:srcdoc="htmlContent"
class="w-full h-full min-h-[400px] html-iframe-wrapper"
class="html-iframe-wrapper h-full min-h-0 w-full"
sandbox="allow-scripts"
data-testid="react-artifact-iframe"
></iframe>
</div>
</template>
Expand Down
67 changes: 22 additions & 45 deletions src/renderer/src/components/artifacts/SvgArtifact.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
<template>
<div class="svg-artifact artifact-dialog-content">
<div
class="artifact-dialog-content flex h-full min-h-0 w-full items-stretch justify-center overflow-auto p-4"
data-testid="svg-artifact-root"
>
<!-- Loading state -->
<div v-if="isLoading" class="loading-message">
<div
v-if="isLoading"
class="flex min-h-full w-full flex-1 flex-col items-center justify-center p-8 text-center"
>
<Icon icon="lucide:loader-2" class="w-6 h-6 animate-spin text-blue-500" />
<p class="text-sm text-muted-foreground mt-2">{{ t('artifacts.sanitizingSvg') }}</p>
</div>

<!-- Error state -->
<div v-else-if="hasError" class="error-message">
<div
v-else-if="hasError"
class="flex min-h-full w-full flex-1 flex-col items-center justify-center p-8 text-center"
>
<Icon icon="lucide:alert-triangle" class="w-6 h-6 text-yellow-500" />
<p class="text-sm text-muted-foreground mt-2">{{ t('artifacts.svgSanitizationFailed') }}</p>
</div>

<!-- Success state - render sanitized content -->
<div class="w-full" v-else-if="sanitizedContent" v-html="sanitizedContent"></div>
<div
v-else-if="sanitizedContent"
class="flex min-h-full w-full flex-1 items-center justify-center [&_svg]:max-h-full [&_svg]:max-w-full [&_svg]:h-auto [&_svg]:w-auto"
data-testid="svg-artifact-content"
v-html="sanitizedContent"
></div>

<!-- Empty state -->
<div v-else class="empty-message">
<div
v-else
class="flex min-h-full w-full flex-1 flex-col items-center justify-center p-8 text-center"
>
<Icon icon="lucide:image" class="w-6 h-6 text-gray-400" />
<p class="text-sm text-muted-foreground mt-2">{{ t('artifacts.noSvgContent') }}</p>
</div>
Expand Down Expand Up @@ -88,43 +105,3 @@ onMounted(() => {
}
})
</script>

<style>
.svg-artifact {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
min-height: 200px;
}

.svg-artifact svg {
max-width: 100%;
height: auto;
}

.loading-message,
.error-message,
.empty-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
}

.loading-message .animate-spin {
animation: spin 1s linear infinite;
}

@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
4 changes: 2 additions & 2 deletions src/renderer/src/components/sidepanel/ChatSidePanel.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<template>
<div
class="relative h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out"
class="relative h-full min-h-0 shrink-0 overflow-hidden transition-[width] duration-200 ease-out"
:style="{ width: `${panelWidth}px` }"
>
<aside
v-if="props.sessionId"
class="absolute inset-y-0 right-0 flex h-full w-full flex-col border-l bg-background shadow-lg transition-all duration-200 ease-out"
class="absolute inset-y-0 right-0 flex h-full min-h-0 w-full flex-col border-l bg-background shadow-lg transition-all duration-200 ease-out"
:class="
shouldShow
? 'translate-x-0 opacity-100'
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/components/sidepanel/WorkspacePanel.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="flex h-full min-w-0 flex-1 overflow-hidden">
<aside class="flex h-full w-[224px] shrink-0 flex-col border-r bg-muted/20">
<div class="flex h-full min-h-0 min-w-0 flex-1 overflow-hidden">
<aside class="flex h-full min-h-0 w-[224px] shrink-0 flex-col border-r bg-muted/20">
<div class="flex-1 overflow-auto py-2">
<section>
<button
Expand Down
14 changes: 10 additions & 4 deletions src/renderer/src/components/sidepanel/WorkspaceViewer.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="flex h-full min-w-0 flex-1 flex-col bg-background">
<div class="flex h-11 items-center justify-between border-b px-3">
<div class="flex h-full min-h-0 min-w-0 flex-1 flex-col bg-background">
<div class="flex h-11 shrink-0 items-center justify-between border-b px-3">
<div class="min-w-0">
<h3 class="truncate text-sm font-medium">{{ viewerTitle }}</h3>
<p v-if="viewerSubtitle" class="truncate text-xs text-muted-foreground">
Expand Down Expand Up @@ -45,7 +45,7 @@
</div>
</div>

<div class="min-h-0 flex-1 overflow-hidden">
<div class="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="workspace-viewer-body">
<div
v-if="paneKind === 'empty' && !(activeSource === 'file' && props.loadingFilePreview)"
class="flex h-full items-center justify-center px-6"
Expand Down Expand Up @@ -98,10 +98,15 @@
</template>
</div>

<WorkspaceCodePane v-else-if="paneKind === 'code' && codeSource" :source="codeSource" />
<WorkspaceCodePane
v-else-if="paneKind === 'code' && codeSource"
class="h-full min-h-0 w-full"
:source="codeSource"
/>

<WorkspacePreviewPane
v-else-if="paneKind === 'preview' && previewKind"
class="h-full min-h-0 w-full"
:session-id="props.sessionId"
:preview-kind="previewKind"
:artifact="previewArtifact"
Expand All @@ -110,6 +115,7 @@

<WorkspaceInfoPane
v-else-if="paneKind === 'info' && props.filePreview"
class="h-full min-h-0 w-full"
:file-preview="props.filePreview"
/>

Expand Down
Loading
Loading