Skip to content
Open
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
4 changes: 3 additions & 1 deletion clawpet-frontend/clawpet/app/desktop-pet-bubble/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { useEffect, useMemo, useRef, useState } from "react"
import { extractPetText } from "@/lib/utils"

import "./bubble.css"

Expand Down Expand Up @@ -72,7 +73,8 @@ export default function DesktopPetBubblePage() {

useEffect(() => {
const handleBubbleShow = (payload: BubbleData) => {
const nextText = typeof payload?.text === "string" ? payload.text.trim() : ""
const rawText = typeof payload?.text === "string" ? payload.text.trim() : ""
const nextText = extractPetText(rawText)
if (!nextText) {
clearHideTimer()
setVisible(false)
Expand Down
71 changes: 71 additions & 0 deletions clawpet-frontend/clawpet/app/desktop-pet/desktop-pet.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@
pointer-events: auto;
width: fit-content;
height: fit-content;
outline: 2px solid transparent;
outline-offset: 4px;
border-radius: 12px;
}

.desktop-pet-area--drag-over {
outline: 3px dashed #667eea;
outline-offset: 12px;
animation: drop-pulse 0.8s ease-in-out infinite alternate;
}

.desktop-pet-area--drag-over .desktop-pet-image {
filter: brightness(0.85);
transition: filter 0.15s ease;
}

@keyframes drop-pulse {
from { outline-color: rgba(102, 126, 234, 0.3); }
to { outline-color: rgba(102, 126, 234, 0.9); }
}

.desktop-pet-area:active {
Expand Down Expand Up @@ -145,3 +164,55 @@
.desktop-pet-state {
display: none;
}

.desktop-pet-btn--active {
background: rgba(102, 126, 234, 0.9);
color: #fff;
}

.desktop-pet-btn--active:hover {
background: rgba(102, 126, 234, 1);
transform: scale(1.1);
}

.desktop-pet-dropzone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
width: 200px;
min-height: 80px;
border: 2px dashed #667eea;
border-radius: 12px;
background: rgba(102, 126, 234, 0.06);
cursor: pointer;
padding: 12px 16px;
transition: all 0.15s ease;
pointer-events: auto;
-webkit-app-region: no-drag;
}

.desktop-pet-dropzone:hover {
background: rgba(102, 126, 234, 0.12);
border-color: #5a6fd6;
}

.desktop-pet-dropzone--drag-over {
background: rgba(102, 126, 234, 0.18);
border-color: #4a5fc6;
border-style: solid;
animation: drop-pulse 0.8s ease-in-out infinite alternate;
}

.desktop-pet-dropzone-icon {
color: #667eea;
opacity: 0.7;
}

.desktop-pet-dropzone-text {
font-size: 11px;
color: #667eea;
text-align: center;
line-height: 1.4;
}
135 changes: 131 additions & 4 deletions clawpet-frontend/clawpet/app/desktop-pet/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client"

import { MessageCircle, X } from "lucide-react"
import { MessageCircle, X, Paperclip } from "lucide-react"
import { useCallback, useEffect, useRef, useState, type MouseEvent } from "react"
import { extractPetText } from "@/lib/utils"

import "./desktop-pet.css"

Expand Down Expand Up @@ -212,6 +213,9 @@ export default function DesktopPetPage() {
const [runtimeHint, setRuntimeHint] = useState("")
const [showControls, setShowControls] = useState(false)
const [voicePhase, setVoicePhase] = useState<"idle" | "recording" | "recognizing" | "error">("idle")
const [isDragOver, setIsDragOver] = useState(false)
const [showDropZone, setShowDropZone] = useState(false)
const dropZoneActiveRef = useRef(false)
const stackRef = useRef<HTMLDivElement | null>(null)
const controlsVisibleRef = useRef(false)
const currentImageRef = useRef(currentImage)
Expand Down Expand Up @@ -369,7 +373,7 @@ export default function DesktopPetPage() {
}

if (data.text !== null) {
setBubble(data.text || "")
setBubble(extractPetText(data.text || ""))
}

const resolvedState = resolvePetState(data)
Expand Down Expand Up @@ -719,6 +723,96 @@ export default function DesktopPetPage() {
window.close()
}

const allowedExtensions = [
".txt", ".md", ".json", ".xml", ".yaml", ".yml", ".csv",
".js", ".ts", ".jsx", ".tsx", ".py", ".go", ".rs", ".java", ".c", ".cpp", ".h",
".css", ".scss", ".html", ".htm", ".sh", ".bash", ".zsh", ".ps1", ".bat", ".cmd",
".sql", ".rb", ".php", ".swift", ".kt", ".scala", ".dart",
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg", ".ico",
]

const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(true)
}

const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.dataTransfer.dropEffect = "copy"
}

const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
setIsDragOver(false)
}

const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragOver(false)
closeDropZone()
const files = e.dataTransfer?.files
if (files && files.length > 0) {
for (let i = 0; i < files.length; i++) {
const f = files[i]
if ((f as any).path) {
window.electronAPI?.sendFileDropped?.((f as any).path, f.name)
}
}
}
transitionTo("think")
}

const handleDropZoneDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragOver(false)
closeDropZone()
const files = e.dataTransfer?.files
if (files && files.length > 0) {
for (let i = 0; i < files.length; i++) {
const f = files[i]
if ((f as any).path) {
window.electronAPI?.sendFileDropped?.((f as any).path, f.name)
}
}
}
transitionTo("think")
}

const handleDropZoneClick = () => {
window.electronAPI?.openFileDialog?.()
closeDropZone()
}

const openDropZone = useCallback(() => {
setShowDropZone(true)
dropZoneActiveRef.current = true
window.electronAPI?.toggleDropZone?.(true)
}, [])

const closeDropZone = useCallback(() => {
setShowDropZone(false)
setIsDragOver(false)
dropZoneActiveRef.current = false
window.electronAPI?.toggleDropZone?.(false)
}, [])

const handleFileDialogClick = () => {
window.electronAPI?.openFileDialog?.()
closeDropZone()
}

// 文件拖放后可开始处理后自动关闭拖放区
useEffect(() => {
const unlisten = window.electronAPI?.onDropZoneAutoClose?.(() => {
if (dropZoneActiveRef.current) {
closeDropZone()
}
})
return () => unlisten?.()
}, [closeDropZone])

const handleOpenSettingsMouseDown = (event: MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()
Expand All @@ -734,7 +828,10 @@ export default function DesktopPetPage() {
return (
<div className="desktop-pet-app">
<div className="desktop-pet-container">
<div ref={stackRef} className="desktop-pet-stack">
<div
ref={stackRef}
className="desktop-pet-stack"
>
<div
className={`desktop-pet-controls ${showControls ? "desktop-pet-controls--visible" : ""}`}
data-electron-no-drag="true"
Expand All @@ -748,6 +845,15 @@ export default function DesktopPetPage() {
>
<MessageCircle className="desktop-pet-btn-icon" />
</button>
<button
type="button"
className={`desktop-pet-btn${showDropZone ? " desktop-pet-btn--active" : ""}`}
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation(); showDropZone ? closeDropZone() : openDropZone() }}
title="Send file"
aria-label="Send file"
>
<Paperclip className="desktop-pet-btn-icon" />
</button>
<button
type="button"
className="desktop-pet-btn desktop-pet-btn-close"
Expand All @@ -759,12 +865,33 @@ export default function DesktopPetPage() {
</button>
</div>
<div
className="desktop-pet-area"
className={`desktop-pet-area${isDragOver ? " desktop-pet-area--drag-over" : ""}`}
data-electron-drag-region="true"
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<img className="desktop-pet-image" src={currentImage} alt="Pet" />
<span className="desktop-pet-state">{petState}</span>
</div>
{showDropZone && (
<div
className={`desktop-pet-dropzone${isDragOver ? " desktop-pet-dropzone--drag-over" : ""}`}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDropZoneDrop}
onClick={handleDropZoneClick}
>
<span className="desktop-pet-dropzone-icon">
<Paperclip size={20} />
</span>
<span className="desktop-pet-dropzone-text">
{isDragOver ? "释放文件" : "拖入文件 / 点击浏览"}
</span>
</div>
)}
{(bubble || runtimeHint) ? (
<div
ref={bubbleRef}
Expand Down
6 changes: 6 additions & 0 deletions clawpet-frontend/clawpet/electron/file-prompt-preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
submitPrompt: (text) => ipcRenderer.send('file-prompt-submit', text || ''),
cancelPrompt: () => ipcRenderer.send('file-prompt-cancel'),
})
Loading