feat: you can summary screen shot directly

This commit is contained in:
2026-07-07 00:59:26 +08:00
parent 39f0aaae02
commit 712a2cbb6a
17 changed files with 1432 additions and 51 deletions
+132 -5
View File
@@ -3,9 +3,7 @@
* App shell — switches between two distinct UI modes that share the same
* Wails window:
*
* • "settings" : full settings UI (self-drawn title bar + sidebar + form).
* Self-drawn because the window is now Frameless to make
* the overlay paint edge-to-edge.
* • "settings" : full settings UI (native title bar + sidebar + form).
* • "overlay" : Snipaste-style region picker that fills the whole
* primary display.
*
@@ -24,6 +22,7 @@ import {
CopyRegionImage,
SaveRegionImage,
SaveRegionToRemote,
SummarizeRegion,
CancelRegion,
} from '../wailsjs/go/main/App'
@@ -39,7 +38,7 @@ const mode = ref<Mode>('settings')
// `SettingsTab` is hoisted to the App shell so the sidebar (which lives
// here) and the inner SettingsView (which renders the matching card) can
// share a single source of truth without an event bus.
type SettingsTab = 'general' | 's3' | 'ssh'
type SettingsTab = 'general' | 's3' | 'ssh' | 'llm'
const activeTab = ref<SettingsTab>('general')
// Sidebar entries are declarative so adding a destination type later is
@@ -48,6 +47,7 @@ const sidebarItems: Array<{ id: SettingsTab; label: string }> = [
{ id: 'general', label: 'General' },
{ id: 's3', label: 'S3' },
{ id: 'ssh', label: 'SSH' },
{ id: 'llm', label: 'LLM' },
]
interface OverlayPayload {
@@ -62,6 +62,14 @@ const capturing = ref(false)
const toast = ref<{ kind: 'success' | 'error'; text: string } | null>(null)
let toastTimer: number | undefined
const operationStatus = ref<{
operation: string
phase: string
message: string
state: 'running' | 'success' | 'error'
} | null>(null)
let operationTimer: number | undefined
function showToast(kind: 'success' | 'error', text: string) {
toast.value = { kind, text }
window.clearTimeout(toastTimer)
@@ -70,6 +78,21 @@ function showToast(kind: 'success' | 'error', text: string) {
}, 3000)
}
function updateOperationStatus(payload: {
operation: string
phase: string
message: string
state: 'running' | 'success' | 'error'
}) {
operationStatus.value = payload
window.clearTimeout(operationTimer)
if (payload.state !== 'running') {
operationTimer = window.setTimeout(() => {
operationStatus.value = null
}, payload.state === 'success' ? 1600 : 3200)
}
}
async function retryHotkey() {
try {
await RetryRegisterHotkey()
@@ -168,6 +191,28 @@ async function onOverlaySaveRemote(rect: {
}
}
async function onOverlaySummarize(rect: {
rect: {
x: number
y: number
w: number
h: number
}
annotations: Array<{
tool: string
color: string
points: Array<{ x: number; y: number }>
}>
}) {
mode.value = 'settings'
overlayPayload.value = null
try {
await SummarizeRegion(rect as any)
} catch {
/* Surfaced via upload:failure */
}
}
async function onOverlayCancel() {
mode.value = 'settings'
overlayPayload.value = null
@@ -194,7 +239,12 @@ onMounted(() => {
capturing.value = false
})
EventsOn('upload:success', (url: string) => {
showToast('success', `Copied: ${url}`)
showToast(
'success',
url === 'summary copied to clipboard'
? 'Summary copied to clipboard'
: `Copied: ${url}`
)
})
EventsOn('upload:failure', (reason: string) => {
showToast('error', reason)
@@ -205,6 +255,7 @@ onMounted(() => {
EventsOn('hotkey:error', (reason: string) => {
hotkeyStatus.value = { state: 'error', reason }
})
EventsOn('operation:status', updateOperationStatus)
})
onUnmounted(() => {
@@ -216,6 +267,7 @@ onUnmounted(() => {
EventsOff('upload:failure')
EventsOff('hotkey:ready')
EventsOff('hotkey:error')
EventsOff('operation:status')
})
</script>
@@ -229,6 +281,7 @@ onUnmounted(() => {
@copy="onOverlayCopy"
@save="onOverlaySave"
@save-remote="onOverlaySaveRemote"
@summarize="onOverlaySummarize"
@cancel="onOverlayCancel"
/>
@@ -264,6 +317,20 @@ onUnmounted(() => {
</main>
<Toast v-if="toast" :kind="toast.kind" :text="toast.text" />
<div
v-if="operationStatus"
class="operation-hud"
:class="operationStatus.state"
>
<span v-if="operationStatus.state === 'running'" class="spinner" />
<span v-else class="status-mark">
{{ operationStatus.state === 'success' ? 'OK' : '!' }}
</span>
<div>
<strong>{{ operationStatus.phase }}</strong>
<p>{{ operationStatus.message }}</p>
</div>
</div>
</div>
</template>
@@ -371,6 +438,66 @@ onUnmounted(() => {
min-width: 0;
}
.operation-hud {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 20;
display: flex;
align-items: center;
gap: 12px;
min-width: 260px;
max-width: 360px;
padding: 12px 14px;
color: #f9fafb;
background: rgba(17, 24, 39, 0.94);
border-radius: 8px;
box-shadow: 0 12px 34px rgba(15, 23, 42, 0.28);
}
.operation-hud strong {
display: block;
margin-bottom: 2px;
font-size: 13px;
}
.operation-hud p {
margin: 0;
color: #d1d5db;
font-size: 12px;
line-height: 1.35;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.25);
border-top-color: #60a5fa;
border-radius: 50%;
animation: spin 0.75s linear infinite;
flex: 0 0 auto;
}
.status-mark {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
font-size: 11px;
font-weight: 700;
flex: 0 0 auto;
}
.operation-hud.success .status-mark {
color: #052e16;
background: #86efac;
}
.operation-hud.error .status-mark {
color: #450a0a;
background: #fca5a5;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-color-scheme: dark) {
.app-root {
background: #1c1d22;