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
22 changes: 22 additions & 0 deletions .changeset/social-graph-atlas-0419.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@xnetjs/sqlite': minor
'@xnetjs/react': minor
---
Comment on lines +1 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use a patch bump for @xnetjs/sqlite.

The PR describes the SQLite change as an FTS fix. It corrects existing indexing behavior and does not add a public SQLite API. Change the release level from minor to patch.

Proposed change
-'`@xnetjs/sqlite`': minor
+'`@xnetjs/sqlite`': patch

As per coding guidelines, fix: produces a patch release; the PR objective describes this change as an FTS fix.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
---
'@xnetjs/sqlite': minor
'@xnetjs/react': minor
---
---
'`@xnetjs/sqlite`': patch
'`@xnetjs/react`': minor
---
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/social-graph-atlas-0419.md around lines 1 - 4, Update the
changeset entry for `@xnetjs/sqlite` from minor to patch, while leaving the
`@xnetjs/react` release level unchanged.

Source: Coding guidelines


🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

manifest="$(fd -a -t f 'package.json' packages/views | head -n1)"
test -n "$manifest"

python - "$manifest" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as file:
    package = json.load(file)

print(package["name"])
PY

rg -n 'SavedViewCanvasProjectionEdge|SavedViewVisualCanvasProjectionRequest' \
  packages/views/src packages/react/src

rg -n '`@xnetjs/views`|packages/views' .changeset

Repository: crs48/xNet

Length of output: 2347


Add @xnetjs/views to this changeset.

packages/views is publishable under @xnetjs/views, and this changeset currently lists only @xnetjs/sqlite and @xnetjs/react. Add @xnetjs/views so the publish tool includes the package for the projection API changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/social-graph-atlas-0419.md around lines 1 - 4, Add the
publishable package `@xnetjs/views` to the package list in the changeset front
matter alongside `@xnetjs/sqlite` and `@xnetjs/react`, preserving the existing
minor release designation.

Source: Coding guidelines


Imported social content is now actually in the full-text index, and a saved
lens can be projected onto a canvas with its relationships.

`extractSearchableContent` read `content`, `description`, `body`, `name` and
`note` — but not `searchText`, the property imported social records
denormalize their full text into precisely so it can be searched. Every
imported post, comment and video transcript was therefore absent from
`nodes_fts` while the pipeline reported it as indexed: search returned a clean
empty result rather than an error. `searchText` is now indexed, with
`textPreview` as the fallback for records that carry no full text. **Existing
databases need `rebuildFTS()` to pick up already-imported rows** — new writes
are indexed correctly from here on.

`SavedViewVisualCanvasProjectionRequest` gains an `edges` array (filtered to
relationships whose endpoints both survived the projection cap), and the new
`SavedViewCanvasProjectionEdge` type is exported. This is what lets a consumer
lay a saved view out as a graph rather than an unconnected grid of cards.
14 changes: 13 additions & 1 deletion apps/electron/src/renderer/components/CanvasView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Rect,
ShapeType
} from '@xnetjs/canvas'
import type { SocialCanvasProjectionPlan } from '@xnetjs/social/projection'
import {
Canvas,
getCanvasObjectsMap,
Expand Down Expand Up @@ -132,6 +133,7 @@
createMindMap: () => boolean
createPlanningTemplate: (templateId: CanvasPlanningTemplateId) => boolean
createQueryFrameFromSavedView: (input: SavedViewCanvasQueryFrameInput) => boolean
applySocialCanvasProjection: (plan: SocialCanvasProjectionPlan) => boolean
refreshSelectedQueryFrame: () => boolean
createExternalReference: (url?: string) => boolean
createMediaFile: () => boolean
Expand Down Expand Up @@ -273,7 +275,8 @@
selectedQueryFrameNode,
selectedQueryFrameDefinition,
createQueryFrameFromSavedView,
refreshSelectedQueryFrame
refreshSelectedQueryFrame,
applySocialCanvasProjection
} = useCanvasQueryFrames({
doc,
sceneRevision,
Expand All @@ -282,6 +285,13 @@
onUndoBoundary: recordSceneUndoBoundary
})

// The imperative handle reports success as a boolean; the hook returns the
// richer result for callers that want the placed/omitted counts.
const applySocialCanvasProjectionHandle = useCallback(
(plan: SocialCanvasProjectionPlan): boolean => Boolean(applySocialCanvasProjection(plan)),
[applySocialCanvasProjection]
)

const selectedDatabaseSourceId =
selectedCanvasObject?.displayType === 'database' ? (selectedCanvasObject.sourceId ?? '') : ''
const undoLadder = useCanvasUndoLadder({
Expand Down Expand Up @@ -396,19 +406,19 @@
canvasRef.current.fitToRect(getNodeRect(targetNode), 140)
return snapshot
},
[doc]

Check warning on line 409 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has missing dependencies: 'canvasRef' and 'lastViewportSnapshotRef'. Either include them or remove the dependency array
)

const restoreViewport = useCallback((snapshot: ViewportSnapshot) => {
lastViewportSnapshotRef.current = snapshot
canvasRef.current?.setViewportSnapshot(snapshot)
}, [])

Check warning on line 415 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has missing dependencies: 'canvasRef' and 'lastViewportSnapshotRef'. Either include them or remove the dependency array

const clearCanvasSelection = useCallback(() => {
closeSelectionPanel()
closePeekSurface()
canvasRef.current?.clearSelection()
}, [closePeekSurface, closeSelectionPanel])

Check warning on line 421 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has a missing dependency: 'canvasRef'. Either include it or remove the dependency array

const zoomCanvas = useCallback((direction: 'out' | 'in'): boolean => {
const handle = canvasRef.current
Expand All @@ -432,7 +442,7 @@
lastViewportSnapshotRef.current = nextSnapshot
handle.setViewportSnapshot(nextSnapshot)
return true
}, [])

Check warning on line 445 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has missing dependencies: 'canvasRef' and 'lastViewportSnapshotRef'. Either include them or remove the dependency array

const fitCanvasContent = useCallback((): boolean => {
const handle = canvasRef.current
Expand All @@ -443,7 +453,7 @@
handle.fitToContent(50)
lastViewportSnapshotRef.current = handle.getViewportSnapshot()
return true
}, [])

Check warning on line 456 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has missing dependencies: 'canvasRef' and 'lastViewportSnapshotRef'. Either include them or remove the dependency array

const resetCanvasView = useCallback((): boolean => {
const handle = canvasRef.current
Expand All @@ -454,7 +464,7 @@
handle.resetView()
lastViewportSnapshotRef.current = handle.getViewportSnapshot()
return true
}, [])

Check warning on line 467 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has missing dependencies: 'canvasRef' and 'lastViewportSnapshotRef'. Either include them or remove the dependency array

const fitSelection = useCallback((): boolean => {
if (selectedNodes.length === 0) {
Expand All @@ -473,11 +483,11 @@

canvasRef.current?.fitToRect(selectionBounds, 140)
return true
}, [selectedNodes])

Check warning on line 486 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has a missing dependency: 'canvasRef'. Either include it or remove the dependency array

const toggleSelectionLock = useCallback((): boolean => {
return canvasRef.current?.toggleSelectionLock() ?? false
}, [])

Check warning on line 490 in apps/electron/src/renderer/components/CanvasView.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useCallback has a missing dependency: 'canvasRef'. Either include it or remove the dependency array

const alignSelection = useCallback(
(alignment: Extract<CanvasAlignment, 'left' | 'right' | 'top' | 'bottom'>): boolean => {
Expand Down Expand Up @@ -784,6 +794,7 @@
createMindMap,
createPlanningTemplate,
createQueryFrameFromSavedView,
applySocialCanvasProjection: applySocialCanvasProjectionHandle,
refreshSelectedQueryFrame,
createExternalReference,
createMediaFile,
Expand All @@ -804,6 +815,7 @@
createMindMap,
createPlanningTemplate,
createQueryFrameFromSavedView,
applySocialCanvasProjectionHandle,
createMediaFile,
createShape,
connectSelection,
Expand Down
13 changes: 10 additions & 3 deletions apps/electron/src/renderer/components/DataWorkspaceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
* and inserting saved lenses onto the canvas as frames.
*/
import { upsertSocialImportJobProgress } from '@xnetjs/social/import/core'
import { useDataWorkspace, DataWorkspaceBody, type SavedViewCanvasFrameInput } from '@xnetjs/views'
import {
useDataWorkspace,
useSocialFeedEnrichment,
DataWorkspaceBody,
type SavedViewCanvasFrameInput
} from '@xnetjs/views'
import { Database, Import, Loader2, X } from 'lucide-react'
import React, { useEffect } from 'react'
import React, { useEffect, useMemo } from 'react'

export type { SavedViewCanvasFrameInput }

Expand All @@ -25,6 +30,8 @@ export function DataWorkspaceView({
onInsertSavedLensAsCanvasFrame
})
const { seeding, handleSeedWorkspace, refreshSocialImportJobs } = workspace
const feedEnrichment = useSocialFeedEnrichment()
const savedViewRunnerProps = useMemo(() => ({ feedEnrichment }), [feedEnrichment])

// Bridge main-process commit jobs into the renderer's import-job store so
// progress started from the main process shows up in the shared panel.
Expand Down Expand Up @@ -84,7 +91,7 @@ export function DataWorkspaceView({
</p>
</div>

<DataWorkspaceBody workspace={workspace} />
<DataWorkspaceBody workspace={workspace} savedViewRunnerProps={savedViewRunnerProps} />
</div>
</div>
</div>
Expand Down
17 changes: 10 additions & 7 deletions apps/electron/src/renderer/shell/use-document-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,15 +385,18 @@ export function useDocumentShell(): DocumentShell {

const handleInsertSavedLensAsCanvasFrame = useCallback(
(view: SavedViewCanvasFrameInput) => {
const inserted =
canvasViewRef.current?.createQueryFrameFromSavedView({
viewId: view.id,
title: view.title ?? 'Saved lens',
descriptorJson: view.descriptor ?? null
}) ?? false
// A projection places the cards and the connections between them; a
// plain lens becomes a live query frame instead (0419).
const inserted = view.projection
? (canvasViewRef.current?.applySocialCanvasProjection(view.projection) ?? false)
: (canvasViewRef.current?.createQueryFrameFromSavedView({
viewId: view.id,
title: view.title ?? 'Saved lens',
descriptorJson: view.descriptor ?? null
}) ?? false)

if (!inserted) {
console.error('Failed to insert saved lens as a canvas query frame', view.id)
console.error('Failed to place saved lens on the canvas', view.id)
return
}

Expand Down
33 changes: 32 additions & 1 deletion apps/web/src/components/CanvasView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
isPeekableCanvasDisplayType,
shouldActivateDatabasePreviewSurface,
shouldActivateInlinePageSurface,
takePendingCanvasLens,
useCanvasCommands,
useCanvasQueryFrames,
useCanvasSourceReferences,
Expand Down Expand Up @@ -279,7 +280,9 @@ export function CanvasView({ docId }: CanvasViewProps): JSX.Element {
queryFrameTargets,
manualQueryFrameRefreshRequests,
selectedQueryFrameDefinition,
refreshSelectedQueryFrame
refreshSelectedQueryFrame,
createQueryFrameFromSavedView,
applySocialCanvasProjection
} = useCanvasQueryFrames({
doc,
sceneRevision,
Expand All @@ -288,6 +291,34 @@ export function CanvasView({ docId }: CanvasViewProps): JSX.Element {
onUndoBoundary: recordSceneUndoBoundary
})

// A lens sent here from the Data Workspace (0419). The workspace parks the
// request and navigates; this claims it once the doc is live. Claiming is
// one-shot, so revisiting the canvas does not re-insert the frame.
useEffect(() => {
if (!doc) return

const pending = takePendingCanvasLens(docId)
if (!pending) return

// A projection places the cards and their connections; without one the
// request is for a live lens frame instead.
if (pending.projection) {
if (!applySocialCanvasProjection(pending.projection)) {
console.error('Failed to project saved lens onto the canvas', pending.viewId)
}
return
}

const inserted = createQueryFrameFromSavedView({
viewId: pending.viewId,
title: pending.title,
descriptorJson: pending.descriptorJson
})
if (!inserted) {
console.error('Failed to insert saved lens as a canvas query frame', pending.viewId)
}
}, [applySocialCanvasProjection, createQueryFrameFromSavedView, doc, docId])

// Peek (0277 E4): modal preview of the selected card's source without
// leaving the board; inline editing activates on zoomed-in selection.
const { peekState, peekedObject, openPeek, closePeekSurface } = useCanvasPeek({
Expand Down
47 changes: 43 additions & 4 deletions apps/web/src/components/DataWorkspaceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,19 @@
* (@xnetjs/views, exploration 0276). Web-specific concerns: OPFS store-backed
* seeding, social feed enrichment, and the moderation render gate.
*/
import { useNavigate } from '@tanstack/react-router'
import { useIdentity } from '@xnetjs/react'
import { useNodeStore } from '@xnetjs/react/internal'
import { useDataWorkspace, DataWorkspaceBody } from '@xnetjs/views'
import {
useDataWorkspace,
DataWorkspaceBody,
stashPendingCanvasLens,
useSocialFeedEnrichment,
type SavedViewCanvasFrameInput
} from '@xnetjs/views'
import { Database, Import, Loader2 } from 'lucide-react'
import { useMemo, type ReactNode } from 'react'
import { useSocialFeedEnrichment } from '../hooks/useSocialFeedEnrichment'
import { useCallback, useMemo, type ReactNode } from 'react'
import { deskIdFor } from '../lib/desk'
import { ModeratedMedia } from './ModeratedMedia'

/**
Expand All @@ -22,10 +30,41 @@ const gateVisualItem = (nodeId: string, content: ReactNode): ReactNode => (
export function DataWorkspaceView(): JSX.Element {
const { store, isReady: storeReady } = useNodeStore()
const feedEnrichment = useSocialFeedEnrichment()
const navigate = useNavigate()
const { identity } = useIdentity()
const did = identity?.did

/**
* Send a saved lens to the Desk as a live query frame.
*
* The Desk is the right target because its id is derived from the identity
* and visiting it provisions it — so this needs no "which canvas?" prompt
* and no separate create. The request is parked, then claimed by the canvas
* once it mounts.
*/
const handleInsertSavedLensAsCanvasFrame = useCallback(
(view: SavedViewCanvasFrameInput) => {
if (!did) return

const canvasId = deskIdFor(did)
stashPendingCanvasLens({
canvasId,
viewId: view.id,
title: view.title ?? 'Saved lens',
descriptorJson: view.descriptor ?? null,
...(view.projection ? { projection: view.projection } : {})
})
void navigate({ to: '/canvas/$canvasId', params: { canvasId } })
},
[did, navigate]
)

const workspace = useDataWorkspace({
seedReady: Boolean(store && storeReady),
getExistingNode: (id) => (store ? Promise.resolve(store.get(id)) : Promise.resolve(undefined))
getExistingNode: (id) => (store ? Promise.resolve(store.get(id)) : Promise.resolve(undefined)),
// Without an identity there is no Desk to target, so the canvas actions
// stay hidden rather than failing when pressed.
...(did ? { onInsertSavedLensAsCanvasFrame: handleInsertSavedLensAsCanvasFrame } : {})
})
const { seeding, seedReady, handleSeedWorkspace } = workspace

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/SavedViewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import { validateSavedViewDescriptor, type SavedViewDescriptor } from '@xnetjs/data'
import { SavedViewSchema } from '@xnetjs/data'
import { SavedViewRunner, useQuery } from '@xnetjs/react'
import { useSocialFeedEnrichment } from '@xnetjs/views'
import { useMemo } from 'react'
import { useSocialFeedEnrichment } from '../hooks/useSocialFeedEnrichment'
import { WORKBENCH_SAVED_VIEW_REGISTRY } from '../lib/saved-view-registry'
import { usePublishTitle } from '../workbench/route-title'

Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/lib/social-import-worker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type BrowserSocialImportStageInput = {
manifest: ArchiveManifest
buckets: string[]
includeSensitive: boolean
fetchTranscripts?: boolean
importedAt?: string
}

Expand Down Expand Up @@ -76,6 +77,7 @@ type MainThreadStagedResult = {
manifest: ArchiveManifest
buckets: string[]
includeSensitive: boolean
fetchTranscripts: boolean
importedAt: string
result: SocialImportNodeDraftStreamResult
streams: Map<string, MainThreadStageDraftStream>
Expand Down Expand Up @@ -316,6 +318,7 @@ async function stageOnMainThread(
readTextEntry,
buckets: input.buckets,
includeSensitive: input.includeSensitive,
fetchTranscripts: input.fetchTranscripts === true,
importedAt,
includeSourceRecords: true,
onComplete: (result) => {
Expand All @@ -331,6 +334,7 @@ async function stageOnMainThread(
manifest: input.manifest,
buckets: input.buckets,
includeSensitive: input.includeSensitive,
fetchTranscripts: input.fetchTranscripts === true,
importedAt,
result,
streams: new Map()
Expand Down Expand Up @@ -376,6 +380,7 @@ export async function stageBrowserSocialArchive(
manifest: input.manifest,
buckets: input.buckets,
includeSensitive: input.includeSensitive,
fetchTranscripts: input.fetchTranscripts === true,
importedAt: input.importedAt
},
isStageResponse
Expand Down Expand Up @@ -496,6 +501,7 @@ async function getMainThreadStageDraftStream(
readTextEntry,
buckets: stagedResult.buckets,
includeSensitive: stagedResult.includeSensitive,
fetchTranscripts: stagedResult.fetchTranscripts,
importedAt: stagedResult.importedAt,
includeSourceRecords
}),
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/social-import-worker-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export type SocialImportWorkerStageRequest = {
manifest: ArchiveManifest
buckets: string[]
includeSensitive: boolean
/** Fetch video transcripts for this run (0419). */
fetchTranscripts?: boolean
importedAt?: string
}

Expand Down
28 changes: 26 additions & 2 deletions apps/web/src/routes/social-import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ function SocialImportPage(): React.ReactElement {
const [selectedBuckets, setSelectedBuckets] = useState<string[]>([])
const [includeSensitive, setIncludeSensitive] = useState(false)
const [includeSourceRecords, setIncludeSourceRecords] = useState(false)
// Transcript fetching is per-run and off by default (0419): the decision is
// about this archive, not a standing preference.
const [fetchTranscripts, setFetchTranscripts] = useState(false)
const [stageResult, setStageResult] = useState<BrowserSocialImportStageResult | null>(null)
const [status, setStatus] = useState<ImportStatus>('idle')
const [error, setError] = useState<string | null>(null)
Expand Down Expand Up @@ -169,15 +172,16 @@ function SocialImportPage(): React.ReactElement {
file: archive.file,
manifest: archive.manifest,
buckets: selectedBuckets,
includeSensitive
includeSensitive,
fetchTranscripts
})
setStageResult(result)
setStatus('staged')
} catch (err) {
setStatus('picked')
setError(toErrorMessage(err))
}
}, [archive, includeSensitive, selectedBuckets])
}, [archive, fetchTranscripts, includeSensitive, selectedBuckets])

const handleCommit = useCallback(async () => {
if (!archive || !stageResult || !store || !storeReady) return
Expand Down Expand Up @@ -678,6 +682,26 @@ function SocialImportPage(): React.ReactElement {
</div>
</div>
</label>
<label className="flex items-start gap-3 rounded-md border border-border p-3">
<input
type="checkbox"
className="mt-1"
checked={fetchTranscripts}
onChange={(event) => {
setFetchTranscripts(event.currentTarget.checked)
setStageResult(null)
setCommitSummary(null)
setCommitProgress(null)
}}
/>
<div>
<div className="text-sm font-medium">Fetch video transcripts</div>
<div className="mt-1 text-xs text-muted-foreground">
Looks up captions for videos this archive brings in, from this device, at a slow
trickle. Videos without captions are recorded as such and never retried.
</div>
</div>
</label>
</section>

<section className="space-y-3">
Expand Down
Loading
Loading