import { useQuery } from '@tanstack/react-query' import { useLocation } from '@tanstack/react-router' import { useServerFn } from '@tanstack/react-start' import { useRef, useState } from 'react' import { AppShell } from '#/components/app-shell' import { Dialog } from '#/components/dialog' import { Icon } from '#/components/icon' import { ConnectionManager } from '#/features/connections/connection-manager' import { loadConnections } from '#/features/connections/server-functions' import { syncResearchDeck } from '#/features/research/research-deck-sync' import { ResearchPanel } from '#/features/research/research-panel' import { sameCitationSource, useCitationNavigation, } from '#/features/research/use-citation-navigation' import { ColumnEditor } from './column-editor' import { type ColumnRegistry, useColumnTools } from './column-tools' import { ResearchColumn } from './deck-column' import { DeckSwitcher } from './deck-switcher' import { type DeckColumn, MAX_COLUMNS, moveColumn } from './model' import { useDeck } from './use-deck' import { useDeckTools } from './webmcp-tools' import './decks.css' export function DeckPage() { const oauthResult = useLocation({ select: (location) => new URLSearchParams(location.searchStr).get('mastodon'), }) const registry = useRef(new Map()).current const researchDeckBindings = useRef(new Map()).current const [editing, setEditing] = useState<{ id: string } | 'new' | null>(null) const [renaming, setRenaming] = useState(false) const [managingConnections, setManagingConnections] = useState(false) const [switcherEditing, setSwitcherEditing] = useState(false) const { deck, workspace, ready, saving, persist, createTemporary, refresh, importLegacy, legacyAvailable, storageError, save, create, select, remove, getWorkspace, } = useDeck(editing !== null || renaming || switcherEditing) const citationNavigation = useCitationNavigation({ registry, getWorkspace, select, createTemporary, }) const fetchProfiles = useServerFn(loadConnections) const profiles = useQuery({ queryKey: ['connections'], queryFn: () => fetchProfiles(), retry: false, }) useColumnTools(registry, ready) const [removed, setRemoved] = useState<{ column: DeckColumn index: number }>() useDeckTools({ ready, saving, storageError, save, select, remove, getWorkspace, createTemporary, persist, connections: profiles.data?.connections, onApplied: clearEditors, }) function clearEditors() { setEditing(null) setRenaming(false) setRemoved(undefined) } async function saveColumn(column: DeckColumn) { const saved = await save({ ...deck, columns: editing === 'new' ? [...deck.columns, column] : deck.columns.map((current) => current.id === column.id ? column : current, ), }) if (saved) setEditing(null) return saved } return ( { await syncResearchDeck(plan, activate, researchDeckBindings, { getWorkspace, createTemporary, save, select, }) if (activate) clearEditors() }} /> } sidebar={ <> { select(id) clearEditors() }} onCreate={async (title) => { const saved = await create(title) if (saved) clearEditors() return saved }} onDelete={async () => { const saved = await remove(deck.id) if (saved) clearEditors() return saved }} />

{deck.persisted ? 'サーバーに保存・端末間で共有' : '一時ビュー · このタブのみ'}

} >

{deck.title}

{deck.columns.length} / {MAX_COLUMNS} カラム
{deck.persisted ? ( ) : ( )}
{!deck.persisted && ready ? (

一時ビューです。再読み込みやタブを閉じると失われます。

) : null} {saving ? (

保存しています…

) : null} {legacyAvailable ? (

このブラウザに旧デッキがあります。

) : null} {!ready ? (

デッキを読み込んでいます…

) : null} {profiles.isError ? (

接続プロファイルを取得できませんでした。

) : null} {oauthResult === 'failed' ? (

Mastodonの認可を完了できませんでした。接続アカウントからやり直してください。

) : null} {oauthResult === 'connected' ? (

Mastodonアカウントを接続しました。カラムの接続先に選べます。

) : null} {profiles.data?.relayError ? (

{profiles.data.relayError}

) : null} {managingConnections ? ( setManagingConnections(false)} > profiles.refetch()} hasTemporaryDecks={workspace.decks.some((deck) => !deck.persisted)} /> ) : null} {profiles.isPending ? (

接続プロファイルを取得しています…

) : null} {storageError ? (

{storageError}

) : null} {renaming ? ( setRenaming(false)}>
{ event.preventDefault() const title = String( new FormData(event.currentTarget).get('title') ?? '', ).trim() if (!title) return if (await save({ ...deck, title })) setRenaming(false) }} >
) : null} {editing !== null ? ( setEditing(null)} > column.id === editing.id) } onSave={saveColumn} onCancel={() => setEditing(null)} /> ) : null} {removed ? (
「{removed.column.title}」を削除しました。
) : null} {ready && deck.columns.length === 0 && editing === null ? (

調査のカラムを並べましょう

検索、ユーザー投稿、リスト。読みたい情報を選んで、ここに追加できます。

) : null} {ready && deck.columns.length > 0 ? ( // biome-ignore lint/a11y/noNoninteractiveTabindex: Keyboard users must be able to scroll the column board.
{deck.columns.map((column, index) => ( connection.id === column.connectionId, )?.displayName } first={index === 0} last={index === deck.columns.length - 1} onEdit={() => setEditing({ id: column.id })} onMove={(direction) => save(moveColumn(deck, column.id, direction)) } onRemove={async () => { const saved = await save({ ...deck, columns: deck.columns.filter( (current) => current.id !== column.id, ), }) if (saved) setRemoved({ column, index }) if (typeof editing === 'object' && editing?.id === column.id) setEditing(null) }} /> ))}
) : null}
) }