@plannotator/ui 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getProviderMeta } from '../ProviderIcons';
|
|
3
|
+
import { type AIProviderOption } from '../../utils/aiProvider';
|
|
4
|
+
|
|
5
|
+
interface AIProviderBarProps {
|
|
6
|
+
providers: AIProviderOption[];
|
|
7
|
+
selectedProviderId: string | null;
|
|
8
|
+
selectedModel: string | null;
|
|
9
|
+
selectedReasoningEffort?: string | null;
|
|
10
|
+
onProviderChange: (providerId: string) => void;
|
|
11
|
+
onModelChange: (model: string) => void;
|
|
12
|
+
onReasoningEffortChange?: (effort: string | null) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AIProviderBar: React.FC<AIProviderBarProps> = ({
|
|
16
|
+
providers,
|
|
17
|
+
selectedProviderId,
|
|
18
|
+
selectedModel,
|
|
19
|
+
selectedReasoningEffort,
|
|
20
|
+
onProviderChange,
|
|
21
|
+
onModelChange,
|
|
22
|
+
onReasoningEffortChange,
|
|
23
|
+
}) => {
|
|
24
|
+
if (providers.length === 0) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="border-t border-border/50 px-2 py-1.5 text-[11px] text-muted-foreground/50">
|
|
27
|
+
No AI providers available
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const currentProvider = providers.find(p => p.id === selectedProviderId) ?? providers[0];
|
|
33
|
+
const effectiveProviderId = currentProvider?.id ?? '';
|
|
34
|
+
const models = currentProvider?.models ?? [];
|
|
35
|
+
const defaultModel = models.find(m => m.default) ?? models[0];
|
|
36
|
+
const effectiveModel = selectedModel ?? defaultModel?.id ?? '';
|
|
37
|
+
const currentModel = models.find(m => m.id === effectiveModel) ?? defaultModel;
|
|
38
|
+
const reasoningEfforts = currentModel?.reasoningEfforts ?? [];
|
|
39
|
+
const meta = getProviderMeta(currentProvider?.name ?? 'AI');
|
|
40
|
+
const Icon = meta.icon;
|
|
41
|
+
// Show only when the selected model actually reports reasoning efforts.
|
|
42
|
+
const showReasoningEffort = !!onReasoningEffortChange && reasoningEfforts.length > 0;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="border-t border-border/50 px-2 py-1.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
46
|
+
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
|
|
47
|
+
<select
|
|
48
|
+
value={effectiveProviderId ?? ''}
|
|
49
|
+
onChange={(event) => onProviderChange(event.target.value)}
|
|
50
|
+
className="min-w-0 max-w-[8rem] bg-transparent text-[11px] text-foreground focus:outline-none"
|
|
51
|
+
aria-label="AI provider"
|
|
52
|
+
>
|
|
53
|
+
{providers.map(provider => {
|
|
54
|
+
const providerMeta = getProviderMeta(provider.name);
|
|
55
|
+
return (
|
|
56
|
+
<option key={provider.id} value={provider.id}>
|
|
57
|
+
{providerMeta.label}
|
|
58
|
+
</option>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
</select>
|
|
62
|
+
|
|
63
|
+
{models.length > 0 && (
|
|
64
|
+
<select
|
|
65
|
+
value={effectiveModel}
|
|
66
|
+
onChange={(event) => onModelChange(event.target.value)}
|
|
67
|
+
className="min-w-0 flex-1 bg-transparent text-[11px] text-foreground focus:outline-none"
|
|
68
|
+
aria-label="AI model"
|
|
69
|
+
>
|
|
70
|
+
{models.map(model => (
|
|
71
|
+
<option key={model.id} value={model.id}>
|
|
72
|
+
{model.label}
|
|
73
|
+
</option>
|
|
74
|
+
))}
|
|
75
|
+
</select>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
{showReasoningEffort && (
|
|
79
|
+
<select
|
|
80
|
+
value={selectedReasoningEffort ?? ''}
|
|
81
|
+
onChange={(event) => onReasoningEffortChange?.(event.target.value || null)}
|
|
82
|
+
className="w-16 bg-transparent text-[11px] text-foreground focus:outline-none"
|
|
83
|
+
aria-label="Reasoning effort"
|
|
84
|
+
>
|
|
85
|
+
<option value="">Auto</option>
|
|
86
|
+
{reasoningEfforts.map(effort => (
|
|
87
|
+
<option key={effort.id} value={effort.id}>
|
|
88
|
+
{effort.label}
|
|
89
|
+
</option>
|
|
90
|
+
))}
|
|
91
|
+
</select>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { AIChatEntry, PendingPermission } from '../../hooks/useAIChat';
|
|
3
|
+
import type { AIProviderOption } from '../../utils/aiProvider';
|
|
4
|
+
import { formatRelativeTime, renderChatMarkdown } from '../../utils/aiChatFormat';
|
|
5
|
+
import { OverlayScrollArea } from '../OverlayScrollArea';
|
|
6
|
+
import { SparklesIcon } from '../SparklesIcon';
|
|
7
|
+
import { AIProviderBar } from './AIProviderBar';
|
|
8
|
+
import { submitHint } from '../../utils/platform';
|
|
9
|
+
|
|
10
|
+
interface DocumentAIChatPanelProps {
|
|
11
|
+
messages: AIChatEntry[];
|
|
12
|
+
isCreatingSession: boolean;
|
|
13
|
+
isStreaming: boolean;
|
|
14
|
+
onAskGeneral?: (question: string) => void;
|
|
15
|
+
/** Stop the in-flight answer. When provided, the input shows a Stop button while streaming. */
|
|
16
|
+
onStop?: () => void;
|
|
17
|
+
permissionRequests?: PendingPermission[];
|
|
18
|
+
onRespondToPermission?: (requestId: string, allow: boolean) => void;
|
|
19
|
+
aiProviders?: AIProviderOption[];
|
|
20
|
+
aiConfig?: { providerId: string | null; model: string | null; reasoningEffort?: string | null };
|
|
21
|
+
onAIConfigChange?: (config: { providerId?: string | null; model?: string | null; reasoningEffort?: string | null }) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function truncate(text: string, max = 180): string {
|
|
25
|
+
if (text.length <= max) return text;
|
|
26
|
+
return `${text.slice(0, max).trimEnd()}...`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatToolInput(toolName: string, input: Record<string, unknown>): string | null {
|
|
30
|
+
if (!input || Object.keys(input).length === 0) return null;
|
|
31
|
+
|
|
32
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
33
|
+
return input.command;
|
|
34
|
+
}
|
|
35
|
+
if ((toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') && typeof input.file_path === 'string') {
|
|
36
|
+
return input.file_path;
|
|
37
|
+
}
|
|
38
|
+
if (toolName === 'Glob' && typeof input.pattern === 'string') {
|
|
39
|
+
return input.pattern;
|
|
40
|
+
}
|
|
41
|
+
if (toolName === 'Grep' && typeof input.pattern === 'string') {
|
|
42
|
+
const path = typeof input.path === 'string' ? ` in ${input.path}` : '';
|
|
43
|
+
return `${input.pattern}${path}`;
|
|
44
|
+
}
|
|
45
|
+
if ((toolName === 'WebFetch' || toolName === 'WebSearch') && typeof input.url === 'string') {
|
|
46
|
+
return input.url;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
return truncate(JSON.stringify(input), 240);
|
|
51
|
+
} catch {
|
|
52
|
+
return String(input);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DocumentAIChatPanel: React.FC<DocumentAIChatPanelProps> = ({
|
|
57
|
+
messages,
|
|
58
|
+
isCreatingSession,
|
|
59
|
+
isStreaming,
|
|
60
|
+
onAskGeneral,
|
|
61
|
+
onStop,
|
|
62
|
+
permissionRequests = [],
|
|
63
|
+
onRespondToPermission,
|
|
64
|
+
aiProviders = [],
|
|
65
|
+
aiConfig,
|
|
66
|
+
onAIConfigChange,
|
|
67
|
+
}) => {
|
|
68
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
69
|
+
const [generalInput, setGeneralInput] = useState('');
|
|
70
|
+
const latestMessage = messages[messages.length - 1];
|
|
71
|
+
const latestResponseText = latestMessage?.response.text ?? '';
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!scrollRef.current) return;
|
|
75
|
+
const last = scrollRef.current.querySelector('[data-ai-message]:last-child');
|
|
76
|
+
last?.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
|
77
|
+
}, [messages.length, latestResponseText]);
|
|
78
|
+
|
|
79
|
+
const handleGeneralSubmit = useCallback(() => {
|
|
80
|
+
const question = generalInput.trim();
|
|
81
|
+
if (!question || !onAskGeneral) return;
|
|
82
|
+
onAskGeneral(question);
|
|
83
|
+
setGeneralInput('');
|
|
84
|
+
}, [generalInput, onAskGeneral]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="flex flex-col h-full">
|
|
88
|
+
<OverlayScrollArea className="flex-1 min-h-0">
|
|
89
|
+
<div ref={scrollRef} className="p-2 space-y-3">
|
|
90
|
+
{messages.length === 0 && !isCreatingSession && (
|
|
91
|
+
<div className="flex flex-col items-center justify-center h-48 text-center px-4 text-muted-foreground">
|
|
92
|
+
<div className="w-10 h-10 rounded-full bg-muted/50 flex items-center justify-center mb-3">
|
|
93
|
+
<SparklesIcon className="w-5 h-5" />
|
|
94
|
+
</div>
|
|
95
|
+
<p className="text-xs">
|
|
96
|
+
{onAskGeneral ? (
|
|
97
|
+
<>Select text and click <strong>Ask AI</strong>, or ask a general question below.</>
|
|
98
|
+
) : (
|
|
99
|
+
<>Select text and click <strong>Ask AI</strong>.</>
|
|
100
|
+
)}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{isCreatingSession && messages.length === 0 && (
|
|
106
|
+
<div className="text-xs text-muted-foreground text-center py-4">
|
|
107
|
+
<span className="ai-streaming-cursor" /> Starting AI session...
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{messages.map(entry => (
|
|
112
|
+
<DocumentQAPair key={entry.question.id} entry={entry} />
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</OverlayScrollArea>
|
|
116
|
+
|
|
117
|
+
{/* Pending permission requests — pinned just above the input/model bar so
|
|
118
|
+
the user sees them right where they act, not buried at the top. */}
|
|
119
|
+
{permissionRequests.filter(p => !p.decided).length > 0 && (
|
|
120
|
+
<div className="border-t border-border/50 p-2 space-y-2">
|
|
121
|
+
{permissionRequests.filter(p => !p.decided).map(permission => (
|
|
122
|
+
<PermissionCard
|
|
123
|
+
key={permission.requestId}
|
|
124
|
+
permission={permission}
|
|
125
|
+
onRespond={onRespondToPermission ?? (() => {})}
|
|
126
|
+
/>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<AIProviderBar
|
|
132
|
+
providers={aiProviders}
|
|
133
|
+
selectedProviderId={aiConfig?.providerId ?? null}
|
|
134
|
+
selectedModel={aiConfig?.model ?? null}
|
|
135
|
+
selectedReasoningEffort={aiConfig?.reasoningEffort ?? null}
|
|
136
|
+
onProviderChange={(providerId) => onAIConfigChange?.({ providerId })}
|
|
137
|
+
onModelChange={(model) => onAIConfigChange?.({ model })}
|
|
138
|
+
onReasoningEffortChange={(reasoningEffort) => onAIConfigChange?.({ reasoningEffort })}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
{onAskGeneral && (
|
|
142
|
+
<GeneralInput
|
|
143
|
+
value={generalInput}
|
|
144
|
+
onChange={setGeneralInput}
|
|
145
|
+
onSubmit={handleGeneralSubmit}
|
|
146
|
+
disabled={isStreaming}
|
|
147
|
+
isStreaming={isStreaming}
|
|
148
|
+
onStop={onStop}
|
|
149
|
+
/>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const DocumentQAPair = memo<{ entry: AIChatEntry }>(({ entry }) => {
|
|
156
|
+
const { question, response } = entry;
|
|
157
|
+
const renderedResponse = useMemo(
|
|
158
|
+
() => response.text ? renderChatMarkdown(response.text) : null,
|
|
159
|
+
[response.text],
|
|
160
|
+
);
|
|
161
|
+
const scope = question.scope;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div data-ai-message data-question-id={question.id} className="flex flex-col gap-1.5">
|
|
165
|
+
<div className="p-2.5 rounded-lg border border-transparent hover:bg-muted/30 transition-colors">
|
|
166
|
+
<div className="flex items-center justify-between gap-2 mb-1">
|
|
167
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
168
|
+
{scope?.kind === 'selection' && (
|
|
169
|
+
<span className="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-primary/10 text-primary">
|
|
170
|
+
selection
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
{scope?.label && (
|
|
174
|
+
<span className="text-[10px] text-muted-foreground truncate">
|
|
175
|
+
{scope.label}
|
|
176
|
+
</span>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
<span className="text-[10px] text-muted-foreground/50 flex-shrink-0">
|
|
180
|
+
{formatRelativeTime(question.createdAt)}
|
|
181
|
+
</span>
|
|
182
|
+
</div>
|
|
183
|
+
{scope?.text && (
|
|
184
|
+
<p className="text-[10px] text-muted-foreground/70 border-l border-border pl-2 mb-1.5 line-clamp-3">
|
|
185
|
+
{truncate(scope.text)}
|
|
186
|
+
</p>
|
|
187
|
+
)}
|
|
188
|
+
<p className="text-xs text-foreground/80 whitespace-pre-wrap">{question.prompt}</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div className="group relative p-2.5 rounded-lg border border-border/50 bg-popover/50 hover:bg-muted/30 transition-colors">
|
|
192
|
+
{response.error ? (
|
|
193
|
+
<p className="text-xs text-destructive">{response.error}</p>
|
|
194
|
+
) : response.text ? (
|
|
195
|
+
<div className="text-xs">
|
|
196
|
+
{renderedResponse}
|
|
197
|
+
{response.isStreaming && <span className="ai-streaming-cursor inline-block ml-0.5" />}
|
|
198
|
+
</div>
|
|
199
|
+
) : response.isStreaming ? (
|
|
200
|
+
<span className="text-xs text-muted-foreground">
|
|
201
|
+
<span className="ai-streaming-cursor" /> Thinking...
|
|
202
|
+
</span>
|
|
203
|
+
) : null}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const PermissionCard: React.FC<{
|
|
210
|
+
permission: PendingPermission;
|
|
211
|
+
onRespond: (requestId: string, allow: boolean) => void;
|
|
212
|
+
}> = ({ permission, onRespond }) => {
|
|
213
|
+
const label = permission.title || permission.displayName || permission.toolName;
|
|
214
|
+
const toolInput = formatToolInput(permission.toolName, permission.toolInput);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="p-2.5 rounded-lg border border-warning/30 bg-warning/5">
|
|
218
|
+
<p className="text-[10px] font-medium text-warning uppercase tracking-wider mb-1">
|
|
219
|
+
Permission Request
|
|
220
|
+
</p>
|
|
221
|
+
<p className="text-xs font-mono text-foreground/80 break-all">
|
|
222
|
+
{label}
|
|
223
|
+
</p>
|
|
224
|
+
{toolInput && (
|
|
225
|
+
<p className="mt-1 px-2 py-1 rounded bg-background/60 border border-warning/20 text-[10px] font-mono text-foreground/80 break-all">
|
|
226
|
+
{toolInput}
|
|
227
|
+
</p>
|
|
228
|
+
)}
|
|
229
|
+
{permission.description && (
|
|
230
|
+
<p className="text-[10px] text-muted-foreground mt-0.5">{permission.description}</p>
|
|
231
|
+
)}
|
|
232
|
+
<div className="flex gap-2 mt-2">
|
|
233
|
+
<button
|
|
234
|
+
onClick={() => onRespond(permission.requestId, true)}
|
|
235
|
+
className="flex-1 px-2 py-1.5 rounded-md bg-primary text-primary-foreground text-[10px] font-medium hover:opacity-90 transition-opacity"
|
|
236
|
+
>
|
|
237
|
+
Allow
|
|
238
|
+
</button>
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => onRespond(permission.requestId, false)}
|
|
241
|
+
className="flex-1 px-2 py-1.5 rounded-md bg-muted text-foreground text-[10px] font-medium hover:bg-muted/80 transition-colors"
|
|
242
|
+
>
|
|
243
|
+
Deny
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const GeneralInput: React.FC<{
|
|
251
|
+
value: string;
|
|
252
|
+
onChange: (value: string) => void;
|
|
253
|
+
onSubmit: () => void;
|
|
254
|
+
disabled?: boolean;
|
|
255
|
+
isStreaming?: boolean;
|
|
256
|
+
onStop?: () => void;
|
|
257
|
+
}> = ({ value, onChange, onSubmit, disabled, isStreaming, onStop }) => {
|
|
258
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
259
|
+
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
const el = textareaRef.current;
|
|
262
|
+
if (!el) return;
|
|
263
|
+
el.style.height = 'auto';
|
|
264
|
+
el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
|
|
265
|
+
}, [value]);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="border-t border-border/50 p-2">
|
|
269
|
+
<div className="flex items-end gap-1.5">
|
|
270
|
+
<textarea
|
|
271
|
+
ref={textareaRef}
|
|
272
|
+
value={value}
|
|
273
|
+
onChange={(event) => onChange(event.target.value)}
|
|
274
|
+
placeholder="Ask about this document..."
|
|
275
|
+
rows={1}
|
|
276
|
+
className="flex-1 px-2.5 py-1.5 bg-muted rounded-md text-xs text-foreground placeholder:text-muted-foreground/50 resize-none focus:outline-none focus:ring-1 focus:ring-primary/50 leading-relaxed"
|
|
277
|
+
style={{ maxHeight: 120 }}
|
|
278
|
+
disabled={disabled}
|
|
279
|
+
onKeyDown={(event) => {
|
|
280
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey) && !event.nativeEvent.isComposing && !disabled) {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
onSubmit();
|
|
283
|
+
}
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
{isStreaming && onStop ? (
|
|
287
|
+
<button
|
|
288
|
+
onClick={onStop}
|
|
289
|
+
className="p-1.5 mb-px rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors flex-shrink-0"
|
|
290
|
+
title="Stop generating"
|
|
291
|
+
aria-label="Stop generating"
|
|
292
|
+
>
|
|
293
|
+
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
|
294
|
+
<rect x="6" y="6" width="12" height="12" rx="2" />
|
|
295
|
+
</svg>
|
|
296
|
+
</button>
|
|
297
|
+
) : (
|
|
298
|
+
<button
|
|
299
|
+
onClick={onSubmit}
|
|
300
|
+
disabled={disabled || !value.trim()}
|
|
301
|
+
className="p-1.5 mb-px rounded-md text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed transition-colors flex-shrink-0"
|
|
302
|
+
title={`Send (${submitHint})`}
|
|
303
|
+
aria-label="Send AI question"
|
|
304
|
+
>
|
|
305
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
306
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
|
307
|
+
</svg>
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AlertKind } from '../../types';
|
|
3
|
+
import { renderProseBody } from './proseBody';
|
|
4
|
+
|
|
5
|
+
interface AlertBlockProps {
|
|
6
|
+
blockId: string;
|
|
7
|
+
kind: AlertKind;
|
|
8
|
+
body: string;
|
|
9
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
10
|
+
onOpenCodeFile?: (path: string) => void;
|
|
11
|
+
imageBaseDir?: string;
|
|
12
|
+
onImageClick?: (src: string, alt: string) => void;
|
|
13
|
+
githubRepo?: string;
|
|
14
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TITLE: Record<AlertKind, string> = {
|
|
18
|
+
note: 'Note',
|
|
19
|
+
tip: 'Tip',
|
|
20
|
+
warning: 'Warning',
|
|
21
|
+
caution: 'Caution',
|
|
22
|
+
important: 'Important',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const Icon: React.FC<{ kind: AlertKind }> = ({ kind }) => {
|
|
26
|
+
const common = { viewBox: '0 0 16 16', width: '16', height: '16', fill: 'currentColor', 'aria-hidden': true as const };
|
|
27
|
+
switch (kind) {
|
|
28
|
+
case 'note':
|
|
29
|
+
return <svg {...common}><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>;
|
|
30
|
+
case 'tip':
|
|
31
|
+
return <svg {...common}><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/></svg>;
|
|
32
|
+
case 'warning':
|
|
33
|
+
return <svg {...common}><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>;
|
|
34
|
+
case 'caution':
|
|
35
|
+
return <svg {...common}><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>;
|
|
36
|
+
case 'important':
|
|
37
|
+
return <svg {...common}><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const AlertBlock: React.FC<AlertBlockProps> = ({
|
|
42
|
+
blockId, kind, body, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, githubRepo, onNavigateAnchor,
|
|
43
|
+
}) => {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={`alert alert-${kind} my-4 pl-4 pr-3 py-2 border-l-[3px]`}
|
|
47
|
+
data-block-id={blockId}
|
|
48
|
+
data-block-type="alert"
|
|
49
|
+
data-alert-kind={kind}
|
|
50
|
+
>
|
|
51
|
+
<div className="alert-title flex items-center gap-2 font-semibold mb-1">
|
|
52
|
+
<Icon kind={kind} />
|
|
53
|
+
<span>{TITLE[kind]}</span>
|
|
54
|
+
</div>
|
|
55
|
+
{renderProseBody({ body, imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor, githubRepo })}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderProseBody } from './proseBody';
|
|
3
|
+
|
|
4
|
+
interface CalloutProps {
|
|
5
|
+
blockId: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
body: string;
|
|
8
|
+
containerClassName: string;
|
|
9
|
+
blockType: 'alert' | 'directive';
|
|
10
|
+
kindAttribute: string;
|
|
11
|
+
onOpenLinkedDoc?: (path: string) => void;
|
|
12
|
+
onOpenCodeFile?: (path: string) => void;
|
|
13
|
+
imageBaseDir?: string;
|
|
14
|
+
onImageClick?: (src: string, alt: string) => void;
|
|
15
|
+
githubRepo?: string;
|
|
16
|
+
onNavigateAnchor?: (hash: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Callout: React.FC<CalloutProps> = ({
|
|
20
|
+
blockId,
|
|
21
|
+
kind,
|
|
22
|
+
body,
|
|
23
|
+
containerClassName,
|
|
24
|
+
blockType,
|
|
25
|
+
kindAttribute,
|
|
26
|
+
onOpenLinkedDoc,
|
|
27
|
+
onOpenCodeFile,
|
|
28
|
+
imageBaseDir,
|
|
29
|
+
onImageClick,
|
|
30
|
+
githubRepo,
|
|
31
|
+
onNavigateAnchor,
|
|
32
|
+
}) => {
|
|
33
|
+
const kindAttr =
|
|
34
|
+
blockType === 'alert' ? { 'data-alert-kind': kindAttribute } : { 'data-directive-kind': kindAttribute };
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className={containerClassName}
|
|
38
|
+
data-block-id={blockId}
|
|
39
|
+
data-block-type={blockType}
|
|
40
|
+
{...kindAttr}
|
|
41
|
+
>
|
|
42
|
+
<div className={`${blockType}-title text-xs font-semibold uppercase tracking-wide mb-1`}>
|
|
43
|
+
{kind}
|
|
44
|
+
</div>
|
|
45
|
+
{renderProseBody({
|
|
46
|
+
body,
|
|
47
|
+
// Callout inherits text color from the container (directive tint per
|
|
48
|
+
// kind). Only pass size/leading classes, not a text-foreground value.
|
|
49
|
+
paragraphClassName: 'text-[15px] leading-relaxed',
|
|
50
|
+
listClassName: 'text-[15px] leading-relaxed',
|
|
51
|
+
imageBaseDir,
|
|
52
|
+
onImageClick,
|
|
53
|
+
onOpenLinkedDoc,
|
|
54
|
+
onOpenCodeFile,
|
|
55
|
+
onNavigateAnchor,
|
|
56
|
+
githubRepo,
|
|
57
|
+
})}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import hljs from 'highlight.js';
|
|
3
|
+
import 'highlight.js/styles/github-dark.css';
|
|
4
|
+
import { Block } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface CodeBlockProps {
|
|
7
|
+
block: Block;
|
|
8
|
+
onHover: (element: HTMLElement) => void;
|
|
9
|
+
onLeave: () => void;
|
|
10
|
+
isHovered: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave, isHovered }) => {
|
|
14
|
+
const [copied, setCopied] = useState(false);
|
|
15
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
const codeRef = useRef<HTMLElement>(null);
|
|
17
|
+
|
|
18
|
+
// Highlight code block on mount and when content/language changes
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (codeRef.current) {
|
|
21
|
+
// Reset any previous highlighting
|
|
22
|
+
codeRef.current.removeAttribute('data-highlighted');
|
|
23
|
+
codeRef.current.className = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
|
|
24
|
+
hljs.highlightElement(codeRef.current);
|
|
25
|
+
}
|
|
26
|
+
}, [block.content, block.language]);
|
|
27
|
+
|
|
28
|
+
const handleCopy = useCallback(async () => {
|
|
29
|
+
try {
|
|
30
|
+
await navigator.clipboard.writeText(block.content);
|
|
31
|
+
setCopied(true);
|
|
32
|
+
setTimeout(() => setCopied(false), 2000);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('Failed to copy:', err);
|
|
35
|
+
}
|
|
36
|
+
}, [block.content]);
|
|
37
|
+
|
|
38
|
+
const handleMouseEnter = () => {
|
|
39
|
+
if (containerRef.current) {
|
|
40
|
+
onHover(containerRef.current);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Build className for code element
|
|
45
|
+
const codeClassName = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
ref={containerRef}
|
|
50
|
+
className="relative group my-5"
|
|
51
|
+
data-block-id={block.id}
|
|
52
|
+
onMouseEnter={handleMouseEnter}
|
|
53
|
+
onMouseLeave={onLeave}
|
|
54
|
+
>
|
|
55
|
+
<button
|
|
56
|
+
onClick={handleCopy}
|
|
57
|
+
className="absolute top-2 right-2 p-1.5 rounded-md bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity z-10"
|
|
58
|
+
title={copied ? 'Copied!' : 'Copy code'}
|
|
59
|
+
>
|
|
60
|
+
{copied ? (
|
|
61
|
+
<svg className="w-4 h-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
62
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
63
|
+
</svg>
|
|
64
|
+
) : (
|
|
65
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
66
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
67
|
+
</svg>
|
|
68
|
+
)}
|
|
69
|
+
</button>
|
|
70
|
+
<pre className="rounded-lg text-[13px] overflow-x-auto bg-muted/50 border border-border/30">
|
|
71
|
+
<code ref={codeRef} className={codeClassName}>{block.content}</code>
|
|
72
|
+
</pre>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|