mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-03 20:41:07 +02:00
add drive button
This commit is contained in:
parent
c548f6bd51
commit
b1e597ee3c
3 changed files with 78 additions and 51 deletions
|
|
@ -5,7 +5,7 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
|
||||||
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { Bug, CheckIcon, LoaderIcon, PanelLeftIcon, ArrowRight, MessageSquare, ChevronLeftIcon, ChevronRightIcon, MoreHorizontal, Plus, HistoryIcon, DownloadIcon, UploadCloud } from 'lucide-react';
|
import { Bug, CheckIcon, LoaderIcon, PanelLeftIcon, ArrowRight, MessageSquare, ChevronLeftIcon, ChevronRightIcon, MoreHorizontal, Plus, HistoryIcon } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
||||||
import { ChatSidebar } from './components/chat-sidebar';
|
import { ChatSidebar } from './components/chat-sidebar';
|
||||||
|
|
@ -1431,8 +1431,8 @@ function App() {
|
||||||
setEditorContent(markdown)
|
setEditorContent(markdown)
|
||||||
}, [setEditorCacheForPath])
|
}, [setEditorCacheForPath])
|
||||||
|
|
||||||
const syncGoogleDocDown = useCallback(async () => {
|
const syncGoogleDocDown = useCallback(async (targetPath?: string) => {
|
||||||
const path = selectedPathRef.current
|
const path = targetPath ?? selectedPathRef.current
|
||||||
if (!path || !path.startsWith('knowledge/') || !path.endsWith('.md')) return
|
if (!path || !path.startsWith('knowledge/') || !path.endsWith('.md')) return
|
||||||
|
|
||||||
setGoogleDocSyncDirection('down')
|
setGoogleDocSyncDirection('down')
|
||||||
|
|
@ -1450,8 +1450,8 @@ function App() {
|
||||||
}
|
}
|
||||||
}, [markRecentLocalMarkdownWrite, reloadMarkdownFileIntoEditor])
|
}, [markRecentLocalMarkdownWrite, reloadMarkdownFileIntoEditor])
|
||||||
|
|
||||||
const syncGoogleDocUp = useCallback(async () => {
|
const syncGoogleDocUp = useCallback(async (targetPath?: string) => {
|
||||||
const path = selectedPathRef.current
|
const path = targetPath ?? selectedPathRef.current
|
||||||
if (!path || !path.startsWith('knowledge/') || !path.endsWith('.md')) return
|
if (!path || !path.startsWith('knowledge/') || !path.endsWith('.md')) return
|
||||||
|
|
||||||
const body = editorContentByPathRef.current.get(path) ?? editorContentRef.current
|
const body = editorContentByPathRef.current.get(path) ?? editorContentRef.current
|
||||||
|
|
@ -5368,10 +5368,6 @@ function App() {
|
||||||
}
|
}
|
||||||
return markdownTabs
|
return markdownTabs
|
||||||
}, [fileTabs, selectedPath])
|
}, [fileTabs, selectedPath])
|
||||||
const selectedLinkedGoogleDoc = React.useMemo(() => {
|
|
||||||
if (!selectedPath?.startsWith('knowledge/') || !selectedPath.endsWith('.md')) return null
|
|
||||||
return parseLinkedGoogleDocFrontmatter(frontmatterByPathRef.current.get(selectedPath) ?? null)
|
|
||||||
}, [selectedPath, editorContent, editorContentByPath])
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
|
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
|
||||||
|
|
@ -5481,46 +5477,6 @@ function App() {
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedLinkedGoogleDoc && (
|
|
||||||
<>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { void syncGoogleDocDown() }}
|
|
||||||
disabled={googleDocSyncDirection !== null || isSaving || Boolean(viewingHistoricalVersion)}
|
|
||||||
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors self-center shrink-0 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
aria-label="Sync down from Google Doc"
|
|
||||||
>
|
|
||||||
{googleDocSyncDirection === 'down' ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<DownloadIcon className="size-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Sync down from Google Doc</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { void syncGoogleDocUp() }}
|
|
||||||
disabled={googleDocSyncDirection !== null || isSaving || Boolean(viewingHistoricalVersion)}
|
|
||||||
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors self-center shrink-0 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
aria-label="Sync up to Google Doc"
|
|
||||||
>
|
|
||||||
{googleDocSyncDirection === 'up' ? (
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<UploadCloud className="size-4" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">Sync up to Google Doc</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedPath && selectedPath.startsWith('knowledge/') && selectedPath.endsWith('.md') && (
|
{selectedPath && selectedPath.startsWith('knowledge/') && selectedPath.endsWith('.md') && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -5794,6 +5750,8 @@ function App() {
|
||||||
? tab.id === activeFileTabId || tab.path === selectedPath
|
? tab.id === activeFileTabId || tab.path === selectedPath
|
||||||
: tab.path === selectedPath
|
: tab.path === selectedPath
|
||||||
const isViewingHistory = viewingHistoricalVersion && isActive && versionHistoryPath === tab.path
|
const isViewingHistory = viewingHistoricalVersion && isActive && versionHistoryPath === tab.path
|
||||||
|
const tabFrontmatter = frontmatterByPathRef.current.get(tab.path) ?? null
|
||||||
|
const linkedGoogleDoc = parseLinkedGoogleDocFrontmatter(tabFrontmatter)
|
||||||
const tabContent = isViewingHistory
|
const tabContent = isViewingHistory
|
||||||
? viewingHistoricalVersion.content
|
? viewingHistoricalVersion.content
|
||||||
: editorContentByPath[tab.path]
|
: editorContentByPath[tab.path]
|
||||||
|
|
@ -5824,7 +5782,7 @@ function App() {
|
||||||
wikiLinks={wikiLinkConfig}
|
wikiLinks={wikiLinkConfig}
|
||||||
onImageUpload={handleImageUpload}
|
onImageUpload={handleImageUpload}
|
||||||
editorSessionKey={editorSessionByTabId[tab.id] ?? 0}
|
editorSessionKey={editorSessionByTabId[tab.id] ?? 0}
|
||||||
frontmatter={frontmatterByPathRef.current.get(tab.path) ?? null}
|
frontmatter={tabFrontmatter}
|
||||||
onFrontmatterChange={(newRaw) => {
|
onFrontmatterChange={(newRaw) => {
|
||||||
frontmatterByPathRef.current.set(tab.path, newRaw)
|
frontmatterByPathRef.current.set(tab.path, newRaw)
|
||||||
// Write updated frontmatter to disk immediately
|
// Write updated frontmatter to disk immediately
|
||||||
|
|
@ -5846,6 +5804,17 @@ function App() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
editable={!isViewingHistory}
|
editable={!isViewingHistory}
|
||||||
|
googleDoc={linkedGoogleDoc && !isViewingHistory ? {
|
||||||
|
title: linkedGoogleDoc.title,
|
||||||
|
isSyncing: isActive ? googleDocSyncDirection : null,
|
||||||
|
onOpen: () => {
|
||||||
|
if (linkedGoogleDoc.url) {
|
||||||
|
window.open(linkedGoogleDoc.url, '_blank')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSyncDown: () => { void syncGoogleDocDown(tab.path) },
|
||||||
|
onSyncUp: () => { void syncGoogleDocUp(tab.path) },
|
||||||
|
} : undefined}
|
||||||
onExport={async (format) => {
|
onExport={async (format) => {
|
||||||
const markdown = tabContent
|
const markdown = tabContent
|
||||||
const title = getBaseName(tab.path)
|
const title = getBaseName(tab.path)
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,21 @@ import {
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
FileTypeIcon,
|
FileTypeIcon,
|
||||||
|
CloudDownloadIcon,
|
||||||
|
LoaderIcon,
|
||||||
|
TriangleIcon,
|
||||||
|
UploadCloudIcon,
|
||||||
Radio,
|
Radio,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
|
@ -45,6 +51,15 @@ interface EditorToolbarProps {
|
||||||
onExport?: (format: 'md' | 'pdf' | 'docx') => void
|
onExport?: (format: 'md' | 'pdf' | 'docx') => void
|
||||||
onOpenLiveNote?: () => void
|
onOpenLiveNote?: () => void
|
||||||
liveState?: LivePillState
|
liveState?: LivePillState
|
||||||
|
googleDoc?: GoogleDocToolbarState
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleDocToolbarState {
|
||||||
|
title: string
|
||||||
|
isSyncing?: 'up' | 'down' | null
|
||||||
|
onOpen: () => void
|
||||||
|
onSyncDown: () => void
|
||||||
|
onSyncUp: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LivePillVariant = 'passive' | 'idle' | 'running' | 'error'
|
export type LivePillVariant = 'passive' | 'idle' | 'running' | 'error'
|
||||||
|
|
@ -67,6 +82,7 @@ export function EditorToolbar({
|
||||||
onExport,
|
onExport,
|
||||||
onOpenLiveNote,
|
onOpenLiveNote,
|
||||||
liveState,
|
liveState,
|
||||||
|
googleDoc,
|
||||||
}: EditorToolbarProps) {
|
}: EditorToolbarProps) {
|
||||||
const [linkUrl, setLinkUrl] = useState('')
|
const [linkUrl, setLinkUrl] = useState('')
|
||||||
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false)
|
||||||
|
|
@ -404,6 +420,45 @@ export function EditorToolbar({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{googleDoc && (
|
||||||
|
<>
|
||||||
|
<div className="separator" />
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 gap-1 px-2"
|
||||||
|
title={`Google Doc: ${googleDoc.title}`}
|
||||||
|
disabled={Boolean(googleDoc.isSyncing)}
|
||||||
|
>
|
||||||
|
{googleDoc.isSyncing ? (
|
||||||
|
<LoaderIcon className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<TriangleIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
<ChevronDownIcon className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={googleDoc.onOpen}>
|
||||||
|
<TriangleIcon className="size-4 mr-2" />
|
||||||
|
Open Google Doc
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={googleDoc.onSyncDown} disabled={Boolean(googleDoc.isSyncing)}>
|
||||||
|
<CloudDownloadIcon className="size-4 mr-2" />
|
||||||
|
Sync down
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={googleDoc.onSyncUp} disabled={Boolean(googleDoc.isSyncing)}>
|
||||||
|
<UploadCloudIcon className="size-4 mr-2" />
|
||||||
|
Sync up
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Live Note pill — pushed to far right */}
|
{/* Live Note pill — pushed to far right */}
|
||||||
{onOpenLiveNote && liveState && (
|
{onOpenLiveNote && liveState && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ function computeWithinBlockOffset(
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
import { EditorToolbar, type LivePillState } from './editor-toolbar'
|
import { EditorToolbar, type GoogleDocToolbarState, type LivePillState } from './editor-toolbar'
|
||||||
import { useLiveNoteForPath } from '@/hooks/use-live-note-for-path'
|
import { useLiveNoteForPath } from '@/hooks/use-live-note-for-path'
|
||||||
import { formatRelativeTime } from '@/lib/relative-time'
|
import { formatRelativeTime } from '@/lib/relative-time'
|
||||||
import { FrontmatterProperties } from './frontmatter-properties'
|
import { FrontmatterProperties } from './frontmatter-properties'
|
||||||
|
|
@ -448,6 +448,7 @@ interface MarkdownEditorProps {
|
||||||
onFrontmatterChange?: (raw: string | null) => void
|
onFrontmatterChange?: (raw: string | null) => void
|
||||||
onExport?: (format: 'md' | 'pdf' | 'docx') => void
|
onExport?: (format: 'md' | 'pdf' | 'docx') => void
|
||||||
notePath?: string
|
notePath?: string
|
||||||
|
googleDoc?: GoogleDocToolbarState
|
||||||
}
|
}
|
||||||
|
|
||||||
type WikiLinkMatch = {
|
type WikiLinkMatch = {
|
||||||
|
|
@ -645,6 +646,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
onFrontmatterChange,
|
onFrontmatterChange,
|
||||||
onExport,
|
onExport,
|
||||||
notePath,
|
notePath,
|
||||||
|
googleDoc,
|
||||||
}, ref) {
|
}, ref) {
|
||||||
const isInternalUpdate = useRef(false)
|
const isInternalUpdate = useRef(false)
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -1628,6 +1630,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorPro
|
||||||
onSelectionHighlight={setSelectionHighlight}
|
onSelectionHighlight={setSelectionHighlight}
|
||||||
onImageUpload={handleImageUploadWithPlaceholder}
|
onImageUpload={handleImageUploadWithPlaceholder}
|
||||||
onExport={onExport}
|
onExport={onExport}
|
||||||
|
googleDoc={googleDoc}
|
||||||
onOpenLiveNote={notePath ? () => {
|
onOpenLiveNote={notePath ? () => {
|
||||||
window.dispatchEvent(new CustomEvent('rowboat:open-live-note-panel', {
|
window.dispatchEvent(new CustomEvent('rowboat:open-live-note-panel', {
|
||||||
detail: { filePath: notePath },
|
detail: { filePath: notePath },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue