├── .gitattributes ├── addon ├── content │ ├── zoteroPane.css │ ├── icons │ │ ├── beaver.png │ │ ├── favicon.png │ │ ├── beaver@0.5x.png │ │ ├── favicon@0.5x.png │ │ ├── beaver_nomargin.png │ │ ├── info.svg │ │ ├── chatting.svg │ │ └── magic-wand.svg │ ├── beaverWindow.xhtml │ └── node_modules_ws_browser_js.reactBundle.js ├── locale │ └── en-US │ │ ├── addon.ftl │ │ ├── mainWindow.ftl │ │ └── preferences.ftl ├── manifest.json └── bootstrap.js ├── react ├── eventBus.ts ├── events │ ├── types.ts │ └── eventManager.ts ├── components │ ├── status │ │ ├── ProgressBar.tsx │ │ └── icons.tsx │ ├── WindowSidebar.tsx │ ├── icons │ │ ├── ArrowDownIcon.tsx │ │ ├── ArrowLeftIcon.tsx │ │ ├── ArrowRightIcon.tsx │ │ ├── ArrowUpIcon.tsx │ │ ├── CircleIcon.tsx │ │ ├── PlayIcon.tsx │ │ ├── UploadCircleIcon.tsx │ │ ├── ArrowUpRightIcon.tsx │ │ ├── CancelCircleIcon.tsx │ │ ├── CheckmarkCircleIcon.tsx │ │ ├── OneIcon.tsx │ │ ├── AlertCircleIcon.tsx │ │ ├── AuthorIcon.tsx │ │ ├── PlusSignIcon.tsx │ │ ├── TickIcon.tsx │ │ ├── FourIcon.tsx │ │ ├── KeyIcon.tsx │ │ ├── TwoIcon.tsx │ │ ├── Share05Icon.tsx │ │ ├── AttachmentIcon.tsx │ │ ├── ViewIcon.tsx │ │ ├── LinkForwardIcon.tsx │ │ ├── InformationCircleIcon.tsx │ │ ├── ThreeIcon.tsx │ │ ├── BookmarkIcon.tsx │ │ ├── CancelIcon.tsx │ │ ├── DollarCircleIcon.tsx │ │ ├── LinkIcon.tsx │ │ ├── Icon.tsx │ │ ├── DownloadIcon.tsx │ │ ├── AlertIcon.tsx │ │ ├── HighlighterIcon.tsx │ │ ├── UserIcon.tsx │ │ ├── TextAlignLeftIcon.tsx │ │ ├── CopyIcon.tsx │ │ ├── MoreHorizontalIcon.tsx │ │ ├── PdfIcon.tsx │ │ ├── ChattingIcon.tsx │ │ ├── DocumentValidationIcon.tsx │ │ ├── StopIcon.tsx │ │ ├── DeleteIcon.tsx │ │ ├── PuzzleIcon.tsx │ │ ├── ClockIcon.tsx │ │ ├── GlobalSearchIcon.tsx │ │ ├── Spinner.tsx │ │ ├── AuthorGroupIcon.tsx │ │ ├── SearchIcon.tsx │ │ ├── RepeatIcon.tsx │ │ ├── LogoutIcon.tsx │ │ ├── BugIcon.tsx │ │ ├── PinIcon.tsx │ │ ├── SyncIcon.tsx │ │ ├── SettingsIcon.tsx │ │ ├── AiMagicIcon.tsx │ │ ├── LibraryIcon.tsx │ │ ├── zotero.tsx │ │ ├── zotero.jsx │ │ ├── CircleStatusIcon.tsx │ │ └── DatabaseIcon.tsx │ ├── agentRuns │ │ ├── RunResumeDisplay.tsx │ │ ├── ReadPagesResultView.tsx │ │ ├── TextPartView.tsx │ │ ├── ItemSearchResultView.tsx │ │ ├── index.ts │ │ ├── ExternalSearchResultView.tsx │ │ ├── FulltextSearchResultView.tsx │ │ ├── ContextCompressionIndicator.tsx │ │ └── AgentActionsDisplay.tsx │ ├── externalReferences │ │ ├── utils.ts │ │ ├── ReferenceMetadataDisplay.tsx │ │ ├── ExternalReferenceListItem.tsx │ │ └── ExternalReferenceDetails.tsx │ ├── previews │ │ ├── SourcePreviewHeading.tsx │ │ ├── SourcePreviewAttachment.tsx │ │ └── ItemsSummaryPreviewContent.tsx │ ├── preferences │ │ ├── ConsentToggle.tsx │ │ ├── AddSelectedItemsOnOpenToggle.tsx │ │ ├── AddSelectedItemsOnNewThreadToggle.tsx │ │ ├── SyncToggle.tsx │ │ └── CitationFormatToggle.tsx │ ├── PreviewAndPopupContainer.tsx │ ├── LibrarySidebar.tsx │ ├── ReaderSidebar.tsx │ ├── messages │ │ └── ToolDisplayFooter.tsx │ ├── ui │ │ ├── popup │ │ │ ├── PopupMessageContainer.tsx │ │ │ └── PopupMessageHeader.tsx │ │ ├── buttons │ │ │ ├── ScrollDownButton.tsx │ │ │ ├── CopyButton.tsx │ │ │ ├── DatabaseStatusButton.tsx │ │ │ └── UserAccountMenuButton.tsx │ │ └── menus │ │ │ └── hooks │ │ │ └── useLibrariesMenu.ts │ ├── pages │ │ └── LoginPage.tsx │ ├── auth │ │ └── otp.ts │ └── dialog │ │ └── ExternalReferenceDetailsDialog.tsx ├── ui │ ├── initialization.ts │ └── types.ts ├── hooks │ ├── useEventSubscription.ts │ ├── useIsomorphicLayoutEffect.ts │ ├── useZoteroSelection.ts │ ├── useSidenavVisibility.ts │ ├── useIndexingCompleteMessage.ts │ ├── useObservePaneCollapse.ts │ ├── useToggleSidebar.ts │ └── useCitationMarker.ts ├── constants │ └── info.ts ├── store.ts ├── types │ ├── attachments │ │ ├── uiTypes.ts │ │ └── converters.ts │ ├── apiErrors.ts │ ├── sources.ts │ ├── popupMessage.ts │ └── agentActions │ │ └── items.ts ├── utils │ ├── clipboard.ts │ ├── openPDFInNewWindow.ts │ ├── stringUtils.ts │ ├── windowContext.ts │ ├── noteActions.ts │ ├── dateUtils.ts │ ├── contentPartUtils.ts │ └── parseTextWithLinksAndNewlines.tsx └── agents │ └── toolResultProcessing.ts ├── src ├── utils │ ├── uuid.ts │ ├── getAPIBaseURL.ts │ ├── window.ts │ ├── logger.ts │ ├── prefs.ts │ ├── hash.ts │ ├── versionNotificationPrefs.ts │ └── ztoolkit.ts ├── ui │ ├── openBeaverWindow.ts │ ├── ChatPane.ts │ └── toggleChat.ts ├── index.ts └── addon.ts ├── .prettierignore ├── docs └── litqa2-accuracy-preview.png ├── .vscode ├── extensions.json ├── settings.json ├── launch.json └── toolkit.code-snippets ├── typings ├── reactBundle.d.ts ├── zotero-types.d.ts ├── i10n.d.ts └── prefs.d.ts ├── .env.development ├── .gitignore ├── .github ├── dependabot.yml ├── renovate.json └── workflows │ ├── release.yml │ └── ci.yml-save ├── tsconfig.json ├── .env.example ├── eslint.config.mjs └── zotero-plugin.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /addon/content/zoteroPane.css: -------------------------------------------------------------------------------- 1 | .makeItRed { 2 | background-color: tomato; 3 | } 4 | -------------------------------------------------------------------------------- /react/eventBus.ts: -------------------------------------------------------------------------------- 1 | const eventBus = new EventTarget(); 2 | export default eventBus; -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | 2 | export function generateUUID(): string { 3 | return crypto.randomUUID(); 4 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | logs 4 | node_modules 5 | package-lock.json 6 | yarn.lock 7 | pnpm-lock.yaml 8 | -------------------------------------------------------------------------------- /addon/content/icons/beaver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/beaver-zotero/HEAD/addon/content/icons/beaver.png -------------------------------------------------------------------------------- /addon/content/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/beaver-zotero/HEAD/addon/content/icons/favicon.png -------------------------------------------------------------------------------- /docs/litqa2-accuracy-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/beaver-zotero/HEAD/docs/litqa2-accuracy-preview.png -------------------------------------------------------------------------------- /addon/content/icons/beaver@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/beaver-zotero/HEAD/addon/content/icons/beaver@0.5x.png -------------------------------------------------------------------------------- /addon/content/icons/favicon@0.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/beaver-zotero/HEAD/addon/content/icons/favicon@0.5x.png -------------------------------------------------------------------------------- /addon/content/icons/beaver_nomargin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/beaver-zotero/HEAD/addon/content/icons/beaver_nomargin.png -------------------------------------------------------------------------------- /src/utils/getAPIBaseURL.ts: -------------------------------------------------------------------------------- 1 | const getAPIBaseURL = () => process.env.API_BASE_URL; 2 | 3 | const API_BASE_URL = getAPIBaseURL() || ''; 4 | export default API_BASE_URL; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "macabeus.vscode-fluent" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnType": false, 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } 9 | -------------------------------------------------------------------------------- /react/events/types.ts: -------------------------------------------------------------------------------- 1 | export interface BeaverEvents { 2 | toggleChat: { 3 | location?: 'library' | 'reader'; 4 | }; 5 | } 6 | 7 | export type BeaverEventName = keyof BeaverEvents; 8 | export type BeaverEventDetail = BeaverEvents[T]; -------------------------------------------------------------------------------- /typings/reactBundle.d.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore reactBundle.js is not a module 2 | declare module "../addon/content/reactBundle.js" { 3 | export function renderAiSidebar(domElement: HTMLElement): void; 4 | export function unmountAiSidebar(domElement: HTMLElement): void; 5 | } -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | SUPABASE_URL=http://localhost:54321 2 | SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 3 | API_BASE_URL=http://localhost:8000 4 | WEBAPP_BASE_URL=http://localhost:3002 -------------------------------------------------------------------------------- /src/ui/openBeaverWindow.ts: -------------------------------------------------------------------------------- 1 | import { BeaverUIFactory } from './ui'; 2 | 3 | /** 4 | * Open Beaver in a separate window. 5 | * If a window already exists, it will be focused instead. 6 | */ 7 | export function openBeaverWindow(): void { 8 | BeaverUIFactory.openBeaverWindow(); 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/utils/window.ts: -------------------------------------------------------------------------------- 1 | export { isWindowAlive }; 2 | 3 | /** 4 | * Check if the window is alive. 5 | * Useful to prevent opening duplicate windows. 6 | * @param win 7 | */ 8 | function isWindowAlive(win?: Window) { 9 | return win && !Components.utils.isDeadWrapper(win) && !win.closed; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | 2 | export const logger = function ( 3 | message: string, 4 | level?: number, 5 | maxDepth?: number, 6 | stack?: number | boolean, 7 | ) { 8 | if ("Beaver" in Zotero && (Zotero as any).Beaver.data.env === "development") { 9 | console.log(`[Beaver] ${message}`); 10 | } 11 | Zotero.debug(`[Beaver] ${message}`, level, maxDepth, stack); 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dot files 2 | .DS_Store 3 | 4 | # Node.js 5 | node_modules 6 | pnpm-lock.yaml 7 | yarn.lock 8 | 9 | # Scaffold 10 | .env 11 | .env.production 12 | .env.staging 13 | .scaffold 14 | build 15 | logs 16 | .cursorrules 17 | .cursor/* 18 | archive 19 | 20 | # compiled files 21 | reactBundle.js 22 | reactBundle.js.map 23 | 24 | 25 | # Build files 26 | /build 27 | /dist 28 | *.js.LICENSE.txt 29 | *.reactBundle.js 30 | -------------------------------------------------------------------------------- /react/components/status/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Progress bar 4 | export const ProgressBar: React.FC<{ progress: number }> = ({ progress }) => ( 5 |
6 |
10 |
11 | ); -------------------------------------------------------------------------------- /addon/locale/en-US/addon.ftl: -------------------------------------------------------------------------------- 1 | startup-begin = Addon is loading 2 | startup-finish = Addon is ready 3 | menuitem-label = Addon Template: Helper Examples 4 | menupopup-label = Addon Template: Menupopup 5 | menuitem-submenulabel = Addon Template 6 | menuitem-filemenulabel = Addon Template: File Menuitem 7 | prefs-title = Template 8 | prefs-table-title = Title 9 | prefs-table-detail = Detail 10 | tabpanel-lib-tab-label = Lib Tab 11 | tabpanel-reader-tab-label = Reader Tab 12 | beaver-menu-upsert = Upsert to Beaver -------------------------------------------------------------------------------- /typings/zotero-types.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface ZoteroItemPane { 5 | collapsed: boolean; 6 | } 7 | 8 | interface ZoteroContextPane { 9 | collapsed: boolean; 10 | togglePane(): void; 11 | } 12 | 13 | interface ZoteroPane { 14 | itemPane: ZoteroItemPane; 15 | } 16 | 17 | interface CustomZoteroWindow extends Window { 18 | ZoteroPane: ZoteroPane; 19 | ZoteroContextPane: ZoteroContextPane; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /react/ui/initialization.ts: -------------------------------------------------------------------------------- 1 | import { uiManager } from './UIManager'; 2 | 3 | export function initializeReactUI(win: Window) { 4 | // Set initial UI state 5 | const isLibraryTab = win.Zotero_Tabs.selectedType === 'library'; 6 | uiManager.updateUI({ 7 | isVisible: false, 8 | isLibraryTab, 9 | collapseState: { 10 | library: null, 11 | reader: null 12 | } 13 | }); 14 | } 15 | 16 | export function cleanupReactUI() { 17 | uiManager.cleanup(); 18 | } -------------------------------------------------------------------------------- /src/ui/ChatPane.ts: -------------------------------------------------------------------------------- 1 | // placeholder illustration 2 | export const chatPaneHTML = ` 3 | 4 | Type questions here: 5 | 6 | 7 | 8 | 9 | 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /react/components/WindowSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Sidebar from './Sidebar'; 3 | 4 | /** 5 | * WindowSidebar is rendered in the separate Beaver window. 6 | * Unlike LibrarySidebar/ReaderSidebar, it's always visible when the window is open. 7 | * The isWindow flag enables window-specific UI behavior (e.g., close button closes the window). 8 | */ 9 | const WindowSidebar = () => { 10 | return ; 11 | }; 12 | 13 | export default WindowSidebar; 14 | 15 | -------------------------------------------------------------------------------- /react/components/icons/ArrowDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ArrowDownIconProps = React.SVGProps; 4 | 5 | const ArrowDownIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default ArrowDownIcon; -------------------------------------------------------------------------------- /react/components/icons/ArrowLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ArrowLeftIconProps = React.SVGProps; 4 | 5 | const ArrowLeftIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default ArrowLeftIcon; -------------------------------------------------------------------------------- /react/hooks/useEventSubscription.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { eventManager } from '../events/eventManager'; 3 | import { BeaverEventName, BeaverEventDetail } from '../events/types'; 4 | 5 | export function useEventSubscription( 6 | eventName: T, 7 | callback: (detail: BeaverEventDetail) => void, 8 | deps: any[] = [] 9 | ) { 10 | useEffect(() => { 11 | const unsubscribe = eventManager.subscribe(eventName, callback); 12 | return unsubscribe; 13 | }, [eventName, ...deps]); 14 | } -------------------------------------------------------------------------------- /addon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__addonName__", 4 | "version": "__buildVersion__", 5 | "description": "__description__", 6 | "homepage_url": "__homepage__", 7 | "author": "__author__", 8 | "icons": { 9 | "48": "content/icons/beaver@0.5x.png", 10 | "96": "content/icons/beaver.png" 11 | }, 12 | "applications": { 13 | "zotero": { 14 | "id": "__addonID__", 15 | "update_url": "__updateURL__", 16 | "strict_min_version": "6.999", 17 | "strict_max_version": "8.*" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /react/components/icons/ArrowRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ArrowRightIconProps = React.SVGProps; 4 | 5 | const ArrowRightIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default ArrowRightIcon; 12 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Start", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": ["run", "start"] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Build", 18 | "runtimeExecutable": "npm", 19 | "runtimeArgs": ["run", "build"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /addon/locale/en-US/mainWindow.ftl: -------------------------------------------------------------------------------- 1 | item-section-example1-head-text = 2 | .label = Plugin Template: Item Info 3 | item-section-example1-sidenav-tooltip = 4 | .tooltiptext = This is Plugin Template section (item info) 5 | item-section-example2-head-text = 6 | .label = Plugin Template: Reader [{$status}] 7 | item-section-example2-sidenav-tooltip = 8 | .tooltiptext = This is Plugin Template section (reader) 9 | item-section-example2-button-tooltip = 10 | .tooltiptext = Unregister this section 11 | item-info-row-example-label = Example Row 12 | item-pane-status = Beaver Status -------------------------------------------------------------------------------- /react/components/icons/ArrowUpIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ArrowUpIconProps = React.SVGProps; 4 | 5 | const ArrowUpIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default ArrowUpIcon; 12 | -------------------------------------------------------------------------------- /react/constants/info.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface InfoItem { 3 | title: string; 4 | description?: string; 5 | url?: string; 6 | tooltip?: string; 7 | } 8 | 9 | export const infoItemList: InfoItem[] = [ 10 | { 11 | title: "File not synced with Beaver", 12 | url: "https://github.com/jlegewie/beaver-zotero/wiki/Syncing-error-in-Beaver", 13 | tooltip: "Get help with this error", 14 | } 15 | ]; 16 | 17 | export const getInfoItemByTitle = (title: string): InfoItem | undefined => { 18 | return infoItemList.find((item) => item.title === title); 19 | }; -------------------------------------------------------------------------------- /src/ui/toggleChat.ts: -------------------------------------------------------------------------------- 1 | import { eventManager } from '../../react/events/eventManager'; 2 | 3 | /** 4 | * Toggle the chat panel on and off. 5 | * 6 | * @param win - The window to toggle the chat in. 7 | * @param turnOn - Whether to turn the chat on or off. 8 | */ 9 | export function triggerToggleChat(win: Window) { 10 | win = Zotero.getMainWindow(); 11 | const selectedType = win.Zotero_Tabs.selectedType; 12 | const location = selectedType === 'library' ? 'library' : 'reader'; 13 | eventManager.dispatch('toggleChat', { 14 | location: location 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /react/components/icons/CircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CircleIconProps = React.SVGProps; 4 | 5 | const CircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default CircleIcon; -------------------------------------------------------------------------------- /addon/locale/en-US/preferences.ftl: -------------------------------------------------------------------------------- 1 | pref-section-account-heading = Account 2 | pref-section-account-text = You are signed in as ... 3 | 4 | pref-section-keys-heading = API Keys 5 | pref-section-keys-text = Providing your own API keys removes any useage limit and allows you to select better models like Gemini 2.5 Pro, Claude 3.7 or Open AI 4o. Read more here. 6 | pref-section-keys-google = Google API Key 7 | pref-section-keys-anthropic = Anthropic API Key 8 | pref-section-keys-openai = OpenAI API Key 9 | 10 | 11 | pref-enable = 12 | .label = Enable 13 | pref-input = Input 14 | pref-help = { $name } Build { $version } { $time } -------------------------------------------------------------------------------- /react/components/icons/PlayIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type PlayIconProps = React.SVGProps; 4 | 5 | const PlayIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default PlayIcon; -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | all-non-major: 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | -------------------------------------------------------------------------------- /react/components/agentRuns/RunResumeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface RunResumeDisplayProps { 4 | runId: string; 5 | } 6 | 7 | /** 8 | * Displays a message for a resumed failed agent run. 9 | */ 10 | export const RunResumeDisplay: React.FC = ({ runId }) => { 11 | return ( 12 |
13 |
14 |
15 | Resuming failed request... 16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default RunResumeDisplay; 23 | -------------------------------------------------------------------------------- /react/ui/types.ts: -------------------------------------------------------------------------------- 1 | export type SidebarLocation = 'library' | 'reader'; 2 | 3 | export interface DOMElements { 4 | chatToggleButton: HTMLElement | null; 5 | libraryPane: HTMLElement | null; 6 | libraryContent: NodeListOf | null; 7 | librarySidebar: HTMLElement | null; 8 | readerPane: HTMLElement | null; 9 | readerContent: NodeListOf | null; 10 | readerSidebar: HTMLElement | null; 11 | } 12 | 13 | export interface CollapseState { 14 | library: boolean | null; 15 | reader: boolean | null; 16 | } 17 | 18 | export interface UIState { 19 | isVisible: boolean; 20 | isLibraryTab: boolean; 21 | collapseState: CollapseState; 22 | } -------------------------------------------------------------------------------- /react/components/icons/UploadCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type UploadCircleIconProps = React.SVGProps; 4 | 5 | const UploadCircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default UploadCircleIcon; -------------------------------------------------------------------------------- /react/components/icons/ArrowUpRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ArrowUpRightIconProps = React.SVGProps; 4 | 5 | const ArrowUpRightIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default ArrowUpRightIcon; 13 | -------------------------------------------------------------------------------- /react/components/icons/CancelCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CancelCircleIconProps = React.SVGProps; 4 | 5 | const CancelCircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default CancelCircleIcon; -------------------------------------------------------------------------------- /react/components/icons/CheckmarkCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CheckmarkCircleIconProps = React.SVGProps; 4 | 5 | const CheckmarkCircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default CheckmarkCircleIcon; -------------------------------------------------------------------------------- /react/components/icons/OneIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type OneIconProps = React.SVGProps; 4 | 5 | const OneIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default OneIcon; 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":semanticPrefixChore", 6 | ":prHourlyLimitNone", 7 | ":prConcurrentLimitNone", 8 | ":enableVulnerabilityAlerts", 9 | ":dependencyDashboard", 10 | "group:allNonMajor", 11 | "schedule:weekly" 12 | ], 13 | "labels": ["dependencies"], 14 | "packageRules": [ 15 | { 16 | "matchPackageNames": [ 17 | "zotero-plugin-toolkit", 18 | "zotero-types", 19 | "zotero-plugin-scaffold" 20 | ], 21 | "schedule": ["at any time"], 22 | "automerge": true 23 | } 24 | ], 25 | "git-submodules": { 26 | "enabled": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "target": "ES2016", 6 | "resolveJsonModule": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "zotero-plugin-toolkit/*": ["node_modules/zotero-plugin-toolkit/*"], 12 | "../../addon/content/reactBundle": ["typings/reactBundle.d.ts"], 13 | "react": ["node_modules/react"], 14 | "react/*": ["node_modules/react/*"] 15 | }, 16 | "jsx": "react", 17 | "esModuleInterop": true, 18 | "lib": ["DOM", "ES2016"] 19 | }, 20 | "include": ["src", "react", "typings", "node_modules/zotero-types"], 21 | "exclude": ["build", "addon"] 22 | } 23 | -------------------------------------------------------------------------------- /react/components/icons/AlertCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type AlertCircleIconProps = React.SVGProps; 4 | 5 | const AlertCircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default AlertCircleIcon; 14 | 15 | -------------------------------------------------------------------------------- /react/components/agentRuns/ReadPagesResultView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChunkReference } from '../../agents/toolResultTypes'; 3 | import ZoteroItemsList from '../ui/ZoteroItemsList'; 4 | 5 | interface ReadPagesResultViewProps { 6 | attachment: ChunkReference; 7 | } 8 | 9 | /** 10 | * Renders the result of a fulltext retrieval tool (read_pages). 11 | * Shows the attachment that was retrieved with optional page info. 12 | */ 13 | export const ReadPagesResultView: React.FC = ({ attachment }) => { 14 | return ( 15 |
16 | 17 |
18 | ); 19 | }; 20 | 21 | export default ReadPagesResultView; 22 | 23 | -------------------------------------------------------------------------------- /addon/content/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /react/components/externalReferences/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Format authors for display. 4 | * @param authors - Array of author names. 5 | * @returns Formatted author string. 6 | */ 7 | export const formatAuthors = (authors?: string[]): string => { 8 | if (!authors || authors.length === 0) return ''; 9 | 10 | const clean = authors.filter(Boolean).map(a => a.trim()); 11 | 12 | if (clean.length === 0) return ''; 13 | 14 | if (clean.length > 3) { 15 | return `${clean[0]} et al.`; 16 | } 17 | 18 | if (clean.length === 1) { 19 | return clean[0]; 20 | } 21 | 22 | if (clean.length === 2) { 23 | return `${clean[0]} and ${clean[1]}`; 24 | } 25 | 26 | // exactly 3 27 | return `${clean[0]}, ${clean[1]} and ${clean[2]}`; 28 | } -------------------------------------------------------------------------------- /react/components/icons/AuthorIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type AuthorIconProps = React.SVGProps; 4 | 5 | const AuthorIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default AuthorIcon; -------------------------------------------------------------------------------- /react/components/icons/PlusSignIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type PlusSignIconProps = React.SVGProps; 4 | 5 | const PlusSignIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default PlusSignIcon; -------------------------------------------------------------------------------- /react/components/icons/TickIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type TickIconProps = React.SVGProps; 4 | 5 | const TickIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default TickIcon; 12 | -------------------------------------------------------------------------------- /react/components/icons/FourIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type FourIconProps = React.SVGProps; 4 | 5 | const FourIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default FourIcon; 13 | -------------------------------------------------------------------------------- /react/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'jotai'; 2 | 3 | /** 4 | * Get or create the shared Jotai store. 5 | * The store is stored on the Zotero object to ensure it's shared across all windows. 6 | * This is necessary because each window loads its own instance of the React bundle. 7 | */ 8 | function getOrCreateStore() { 9 | // Check if we're in a Zotero environment 10 | if (typeof Zotero !== 'undefined') { 11 | // Use existing store if available, otherwise create and store it 12 | if (!Zotero.__beaverJotaiStore) { 13 | Zotero.__beaverJotaiStore = createStore(); 14 | } 15 | return Zotero.__beaverJotaiStore; 16 | } 17 | // Fallback for non-Zotero environments (shouldn't happen in practice) 18 | return createStore(); 19 | } 20 | 21 | export const store = getOrCreateStore(); 22 | -------------------------------------------------------------------------------- /react/components/icons/KeyIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type KeyIconProps = React.SVGProps; 4 | 5 | const KeyIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default KeyIcon; 13 | -------------------------------------------------------------------------------- /react/components/icons/TwoIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type TwoIconProps = React.SVGProps; 4 | 5 | const TwoIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default TwoIcon; 13 | 14 | 15 | -------------------------------------------------------------------------------- /react/components/icons/Share05Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Share05IconProps = React.SVGProps; 4 | 5 | const Share05Icon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default Share05Icon; -------------------------------------------------------------------------------- /react/components/icons/AttachmentIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type AttachmentIconProps = React.SVGProps; 4 | 5 | const AttachmentIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default AttachmentIcon; -------------------------------------------------------------------------------- /react/components/icons/ViewIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ViewIconProps = React.SVGProps; 4 | 5 | const ViewIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default ViewIcon; -------------------------------------------------------------------------------- /react/components/previews/SourcePreviewHeading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CSSItemTypeIcon } from '../icons/icons'; 3 | import { InputSource } from '../../types/sources'; 4 | import { getDisplayNameFromItem } from '../../utils/sourceUtils'; 5 | 6 | interface SourcePreviewHeadingProps { 7 | source: InputSource; 8 | item: Zotero.Item; 9 | } 10 | 11 | const SourcePreviewHeading: React.FC = ({ source, item }) => { 12 | return ( 13 | 14 | 15 | {} 16 | 17 | {getDisplayNameFromItem(item, source.childItemKeys.length)} 18 | 19 | ); 20 | }; 21 | 22 | export default SourcePreviewHeading; 23 | -------------------------------------------------------------------------------- /react/components/preferences/ConsentToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PreferenceToggle from "./PreferenceToggle"; 3 | 4 | interface ConsentToggleProps { 5 | checked: boolean; 6 | onChange: (checked: boolean) => void; 7 | } 8 | 9 | const ConsentToggle: React.FC = ({ checked, onChange }) => { 10 | return ( 11 | 18 | ); 19 | }; 20 | 21 | export default ConsentToggle; -------------------------------------------------------------------------------- /react/components/icons/LinkForwardIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type LinkForwardIconProps = React.SVGProps; 4 | 5 | const LinkForwardIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default LinkForwardIcon; -------------------------------------------------------------------------------- /react/components/icons/InformationCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type InformationCircleIconProps = React.SVGProps; 4 | 5 | const InformationCircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default InformationCircleIcon; 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { BasicTool } from "zotero-plugin-toolkit"; 2 | import Addon from "./addon"; 3 | import { config } from "../package.json"; 4 | 5 | const basicTool = new BasicTool(); 6 | 7 | // @ts-ignore - Plugin instance is not typed 8 | if (!basicTool.getGlobal("Zotero")[config.addonInstance]) { 9 | _globalThis.addon = new Addon(); 10 | defineGlobal("ztoolkit", () => { 11 | return _globalThis.addon.data.ztoolkit; 12 | }); 13 | // @ts-ignore - Plugin instance is not typed 14 | Zotero[config.addonInstance] = addon; 15 | } 16 | 17 | function defineGlobal(name: Parameters[0]): void; 18 | function defineGlobal(name: string, getter: () => any): void; 19 | function defineGlobal(name: string, getter?: () => any) { 20 | Object.defineProperty(_globalThis, name, { 21 | get() { 22 | return getter ? getter() : basicTool.getGlobal(name); 23 | }, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /react/components/PreviewAndPopupContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAtomValue } from 'jotai'; 3 | import PopupMessageContainer from './ui/popup/PopupMessageContainer'; 4 | import PreviewContainer from './previews/PreviewContainer'; 5 | import { popupMessagesAtom } from '../atoms/ui'; 6 | 7 | // Preview and popup container component 8 | const PreviewAndPopupContainer: React.FC = () => { 9 | const popupMessages = useAtomValue(popupMessagesAtom); 10 | const hasPopupMessages = popupMessages.length > 0; 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default PreviewAndPopupContainer; 21 | -------------------------------------------------------------------------------- /react/components/agentRuns/TextPartView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextPart } from '../../agents/types'; 3 | import MarkdownRenderer from '../messages/MarkdownRenderer'; 4 | 5 | interface TextPartViewProps { 6 | part: TextPart; 7 | /** Agent run ID for linking citations and saving notes */ 8 | runId?: string; 9 | } 10 | 11 | /** 12 | * Renders a text part with markdown support. 13 | * Since WSPartEvent sends accumulated content (not deltas), 14 | * we simply render the current content state. 15 | */ 16 | export const TextPartView: React.FC = ({ part, runId }) => { 17 | if (!part.content || part.content.trim() === '') { 18 | return null; 19 | } 20 | 21 | return ( 22 | 27 | ); 28 | }; 29 | 30 | export default TextPartView; 31 | 32 | -------------------------------------------------------------------------------- /react/components/icons/ThreeIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ThreeIconProps = React.SVGProps; 4 | 5 | const ThreeIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default ThreeIcon; 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /react/types/attachments/uiTypes.ts: -------------------------------------------------------------------------------- 1 | import { SourceAttachment, AnnotationAttachment, NoteAttachment, MessageAttachment, ItemMetadataAttachment } from "./apiTypes"; 2 | 3 | /** 4 | * Attachment with messageId and optional item. 5 | * The item is populated lazily when needed - attachments start as metadata-only 6 | * and items are loaded on-demand via getZoteroItem() or similar. 7 | */ 8 | export interface SourceAttachmentWithId extends SourceAttachment { 9 | messageId: string; 10 | item?: Zotero.Item; 11 | } 12 | 13 | export interface AnnotationAttachmentWithId extends AnnotationAttachment { 14 | messageId: string; 15 | item?: Zotero.Item; 16 | } 17 | 18 | export interface NoteAttachmentWithId extends NoteAttachment { 19 | messageId: string; 20 | item?: Zotero.Item; 21 | } 22 | 23 | export interface ItemMetadataAttachmentWithId extends ItemMetadataAttachment { 24 | messageId: string; 25 | item?: Zotero.Item; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /react/components/icons/BookmarkIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type BookmarkIconProps = React.SVGProps; 4 | 5 | const BookmarkIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default BookmarkIcon; -------------------------------------------------------------------------------- /react/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | 3 | /** 4 | * Isomorphic layout effect that works in both client and SSR contexts. 5 | * 6 | * Uses useLayoutEffect on client (avoids visual flicker by running synchronously 7 | * after DOM mutations but before paint) and useEffect during SSR (where 8 | * useLayoutEffect would throw a warning since there's no DOM to layout). 9 | * 10 | * Note: During SSR (e.g., renderToStaticMarkup), effects don't run at all. 11 | * This hook only suppresses the warning; SSR components should handle 12 | * the no-effect case via synchronous fallbacks in their render logic. 13 | */ 14 | export const useIsomorphicLayoutEffect = 15 | typeof Zotero.getMainWindow() !== 'undefined' && 16 | typeof Zotero.getMainWindow().document !== 'undefined' && 17 | typeof Zotero.getMainWindow().document.createElement === 'function' 18 | ? useLayoutEffect 19 | : useEffect; 20 | 21 | -------------------------------------------------------------------------------- /react/components/LibrarySidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAtomValue } from "jotai"; 3 | import Sidebar from "./Sidebar"; 4 | import { isSidebarVisibleAtom, isLibraryTabAtom } from "../atoms/ui"; 5 | import { useObservePaneCollapse } from '../hooks/useObservePaneCollapse'; 6 | 7 | // LibrarySidebarContent handles library-specific features 8 | const LibrarySidebarContent = () => { 9 | 10 | // Watch for pane collapse 11 | useObservePaneCollapse("library"); 12 | 13 | // Render the sidebar 14 | return ; 15 | } 16 | 17 | // LibrarySidebar handles visibility 18 | const LibrarySidebar = () => { 19 | const isVisible = useAtomValue(isSidebarVisibleAtom); 20 | const isLibraryTab = useAtomValue(isLibraryTabAtom); 21 | 22 | // Return the sidebar if it is visible and the currently selected tab is a library tab 23 | return isVisible && isLibraryTab ? : null; 24 | } 25 | 26 | export default LibrarySidebar; -------------------------------------------------------------------------------- /react/components/icons/CancelIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CancelIconProps = React.SVGProps; 4 | 5 | const CancelIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default CancelIcon; -------------------------------------------------------------------------------- /src/utils/prefs.ts: -------------------------------------------------------------------------------- 1 | import config from "../../package.json"; 2 | 3 | type PluginPrefsMap = _ZoteroTypes.Prefs["PluginPrefsMap"]; 4 | 5 | const PREFS_PREFIX = config.config.prefsPrefix; 6 | 7 | /** 8 | * Get preference value. 9 | * Wrapper of `Zotero.Prefs.get`. 10 | * @param key 11 | */ 12 | export function getPref(key: K) { 13 | return Zotero.Prefs.get(`${PREFS_PREFIX}.${key}`, true) as PluginPrefsMap[K]; 14 | } 15 | 16 | /** 17 | * Set preference value. 18 | * Wrapper of `Zotero.Prefs.set`. 19 | * @param key 20 | * @param value 21 | */ 22 | export function setPref( 23 | key: K, 24 | value: PluginPrefsMap[K], 25 | ) { 26 | return Zotero.Prefs.set(`${PREFS_PREFIX}.${key}`, value, true); 27 | } 28 | 29 | /** 30 | * Clear preference value. 31 | * Wrapper of `Zotero.Prefs.clear`. 32 | * @param key 33 | */ 34 | export function clearPref(key: string) { 35 | return Zotero.Prefs.clear(`${PREFS_PREFIX}.${key}`, true); 36 | } 37 | -------------------------------------------------------------------------------- /react/components/agentRuns/ItemSearchResultView.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ZoteroItemReference } from '../../types/zotero'; 3 | import ZoteroItemsList from '../ui/ZoteroItemsList'; 4 | 5 | interface ItemSearchResultViewProps { 6 | items: ZoteroItemReference[]; 7 | } 8 | 9 | /** 10 | * Renders the result of an item search tool (search_references_by_topic, search_references_by_metadata). 11 | * Uses ZoteroItemsList to display the items with clickable links to reveal in Zotero. 12 | */ 13 | export const ItemSearchResultView: React.FC = ({ items }) => { 14 | if (items.length === 0) { 15 | return ( 16 |
17 | No items found 18 |
19 | ); 20 | } 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }; 28 | 29 | export default ItemSearchResultView; 30 | 31 | -------------------------------------------------------------------------------- /react/components/icons/DollarCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type DollarCircleIconProps = React.SVGProps; 4 | 5 | const DollarCircleIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default DollarCircleIcon; -------------------------------------------------------------------------------- /react/components/icons/LinkIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type LinkIconProps = React.SVGProps; 4 | 5 | const LinkIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default LinkIcon; -------------------------------------------------------------------------------- /react/components/status/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CheckmarkCircleIcon, CancelCircleIcon, Icon, Spinner, ThreeIcon, OneIcon, TwoIcon, AlertIcon as AlertIconIcon, InformationCircleIcon as InformationCircleIconIcon } from "../icons/icons"; 3 | 4 | 5 | export const CancelIcon = ; 6 | export const CheckmarkIconGrey = ; 7 | export const CheckmarkIcon = ; 8 | export const StepOneIcon = ; 9 | export const StepTwoIcon = ; 10 | export const StepThreeIcon = ; 11 | export const SpinnerIcon = ; 12 | export const AlertIcon = ; 13 | -------------------------------------------------------------------------------- /react/components/icons/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface IconProps { 4 | icon: React.ComponentType>; 5 | size?: number | string; 6 | color?: string; 7 | className?: string; 8 | } 9 | 10 | const Icon: React.FC = ({ 11 | icon: IconComponent, 12 | size = '1em', // Default to 1em to scale with text 13 | color = 'currentColor', 14 | className = '', 15 | ...props 16 | }) => { 17 | // Remove the wrapper div and directly render the SVG 18 | return ( 19 | 32 | ); 33 | }; 34 | 35 | export default Icon; -------------------------------------------------------------------------------- /react/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options for the copyToClipboard function 3 | */ 4 | interface CopyToClipboardOptions { 5 | /** Callback function that runs after successful copying */ 6 | onSuccess?: (text: string) => void; 7 | /** Callback function that runs if copying fails */ 8 | onError?: (error: Error) => void; 9 | } 10 | 11 | /** 12 | * Copies text to the clipboard 13 | * 14 | * @param text The text to copy 15 | * @param options Optional callbacks for success/error 16 | * @returns A promise that resolves when copying is complete 17 | */ 18 | export async function copyToClipboard( 19 | text: string, 20 | options: CopyToClipboardOptions = {} 21 | ): Promise { 22 | const { onSuccess, onError } = options; 23 | 24 | try { 25 | await navigator.clipboard.writeText(text); 26 | 27 | if (onSuccess) { 28 | onSuccess(text); 29 | } 30 | 31 | return true; 32 | } catch (error) { 33 | if (onError) { 34 | onError(error as Error); 35 | } 36 | 37 | return false; 38 | } 39 | } -------------------------------------------------------------------------------- /react/components/ReaderSidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAtomValue } from "jotai"; 3 | import Sidebar from "./Sidebar"; 4 | import { isSidebarVisibleAtom, isLibraryTabAtom } from "../atoms/ui"; 5 | import { useObservePaneCollapse } from '../hooks/useObservePaneCollapse'; 6 | import { useRecentThreads } from '../hooks/useRecentThreads'; 7 | import { useReaderTabSelection } from '../hooks/useReaderTabSelection'; 8 | 9 | // ReaderSidebarContent handles library-specific features 10 | const ReaderSidebarContent = () => { 11 | 12 | useReaderTabSelection(); 13 | 14 | useObservePaneCollapse("reader"); 15 | // Recent threads subscription 16 | // useRecentThreads(); 17 | // Render the sidebar 18 | return ; 19 | } 20 | 21 | const ReaderSidebar = () => { 22 | const isVisible = useAtomValue(isSidebarVisibleAtom); 23 | const isLibraryTab = useAtomValue(isLibraryTabAtom); 24 | 25 | return (isVisible && !isLibraryTab) ? : null; 26 | } 27 | 28 | export default ReaderSidebar; -------------------------------------------------------------------------------- /react/components/icons/DownloadIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Download01IconProps = React.SVGProps; 4 | 5 | const Download01Icon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default Download01Icon; -------------------------------------------------------------------------------- /addon/content/icons/chatting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /react/types/apiErrors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom error class for API-related errors 3 | */ 4 | export class ApiError extends Error { 5 | public readonly status: number; 6 | public readonly statusText: string; 7 | 8 | constructor(status: number, statusText: string, message?: string) { 9 | super(message || `API error: ${status} - ${statusText}`); 10 | this.name = 'ApiError'; 11 | this.status = status; 12 | this.statusText = statusText; 13 | } 14 | } 15 | 16 | /** 17 | * Error thrown when Zotero instance is not linked to the user account 18 | */ 19 | export class ZoteroInstanceMismatchError extends Error { 20 | constructor() { 21 | super('This Zotero instance is not linked to your account'); 22 | this.name = 'ZoteroInstanceMismatchError'; 23 | } 24 | } 25 | 26 | /** 27 | * Error thrown for server-side errors 28 | */ 29 | export class ServerError extends Error { 30 | constructor(message?: string) { 31 | super(message || 'Server error occurred'); 32 | this.name = 'ServerError'; 33 | } 34 | } -------------------------------------------------------------------------------- /react/types/sources.ts: -------------------------------------------------------------------------------- 1 | import { TextSelection } from "./attachments/apiTypes"; 2 | 3 | export interface InputSource { 4 | id: string; // Unique identifier 5 | type: "regularItem" | "attachment" | "note" | "annotation" | "reader"; // Type of source 6 | messageId?: string; // Message ID for tracking 7 | libraryID: number; // Zotero library ID 8 | itemKey: string; // Zotero item key 9 | pinned: boolean; // If true, the source persists across selections 10 | parentKey: string | null; // Key of the parent item 11 | childItemKeys: string[]; // Keys of child items 12 | timestamp: number; // Creation timestamp 13 | textSelection?: TextSelection; 14 | isValid?: boolean; // If true, the source is valid 15 | invalidReason?: string; // Reason for invalidity (if isValid is false) 16 | } 17 | 18 | export type ReaderSource = InputSource & { type: "reader" }; 19 | 20 | // export type ThreadSource = Omit & { 21 | // type: "attachment" | "note" | "annotation" | "reader"; 22 | // }; 23 | -------------------------------------------------------------------------------- /react/components/icons/AlertIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type AlertIconProps = React.SVGProps; 4 | 5 | const AlertIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | 13 | export default AlertIcon; -------------------------------------------------------------------------------- /react/components/icons/HighlighterIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type HighlighterIconProps = React.SVGProps; 4 | 5 | const HighlighterIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | export default HighlighterIcon; -------------------------------------------------------------------------------- /react/components/agentRuns/index.ts: -------------------------------------------------------------------------------- 1 | // Agent Run Components 2 | export { ThreadView } from './ThreadView'; 3 | export { AgentRunView } from './AgentRunView'; 4 | export { UserRequestView } from './UserRequestView'; 5 | export { ModelMessagesView } from './ModelMessagesView'; 6 | export { ModelResponseView } from './ModelResponseView'; 7 | export { TextPartView } from './TextPartView'; 8 | export { ThinkingPartView } from './ThinkingPartView'; 9 | export { ToolCallPartView } from './ToolCallPartView'; 10 | export { ToolResultView } from './ToolResultView'; 11 | export { ItemSearchResultView } from './ItemSearchResultView'; 12 | export { AnnotationToolCallView } from './AnnotationToolCallView'; 13 | export { AgentRunFooter } from './AgentRunFooter'; 14 | export { RunStatusIndicator } from './RunStatusIndicator'; 15 | export { TokenUsageDisplay } from './TokenUsageDisplay'; 16 | export { AgentActionsDisplay } from './AgentActionsDisplay'; 17 | export { default as CreateItemAgentActionDisplay } from './CreateItemAgentActionDisplay'; 18 | export { default as AgentActionItemButtons } from './AgentActionItemButtons'; 19 | 20 | -------------------------------------------------------------------------------- /react/components/messages/ToolDisplayFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Icon, ArrowUpIcon } from '../icons/icons'; 3 | 4 | interface ToolDisplayFooterProps { 5 | toggleContent: () => void; 6 | } 7 | 8 | export const ToolDisplayFooter = React.memo(function ToolDisplayFooter(props: ToolDisplayFooterProps) { 9 | const { toggleContent } = props; 10 | const [isHovered, setIsHovered] = useState(false); 11 | 12 | return ( 13 |
setIsHovered(true)} 17 | onMouseLeave={() => setIsHovered(false)} 18 | > 19 | 24 |
25 | ); 26 | }); -------------------------------------------------------------------------------- /typings/i10n.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by zotero-plugin-scaffold 2 | /* prettier-ignore */ 3 | /* eslint-disable */ 4 | // @ts-nocheck 5 | export type FluentMessageId = 6 | | 'beaver-menu-upsert' 7 | | 'item-info-row-example-label' 8 | | 'item-pane-status' 9 | | 'item-section-example1-head-text' 10 | | 'item-section-example1-sidenav-tooltip' 11 | | 'item-section-example2-button-tooltip' 12 | | 'item-section-example2-head-text' 13 | | 'item-section-example2-sidenav-tooltip' 14 | | 'menuitem-filemenulabel' 15 | | 'menuitem-label' 16 | | 'menuitem-submenulabel' 17 | | 'menupopup-label' 18 | | 'pref-enable' 19 | | 'pref-help' 20 | | 'pref-input' 21 | | 'pref-section-account-heading' 22 | | 'pref-section-account-text' 23 | | 'pref-section-keys-anthropic' 24 | | 'pref-section-keys-google' 25 | | 'pref-section-keys-heading' 26 | | 'pref-section-keys-openai' 27 | | 'pref-section-keys-text' 28 | | 'prefs-table-detail' 29 | | 'prefs-table-title' 30 | | 'prefs-title' 31 | | 'startup-begin' 32 | | 'startup-finish' 33 | | 'tabpanel-lib-tab-label' 34 | | 'tabpanel-reader-tab-label'; 35 | -------------------------------------------------------------------------------- /react/components/ui/popup/PopupMessageContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAtomValue } from 'jotai'; 3 | import { popupMessagesAtom } from '../../../atoms/ui'; 4 | import PopupMessageItem from './PopupMessageItem'; 5 | 6 | interface PopupMessageContainerProps { 7 | className?: string; 8 | } 9 | 10 | const MAX_MESSAGES = 3; 11 | 12 | const PopupMessageContainer: React.FC = ({ className }) => { 13 | const messages = useAtomValue(popupMessagesAtom); 14 | 15 | if (!messages.length) { 16 | return null; 17 | } 18 | 19 | const containerClassName = ['flex flex-col-reverse gap-2', className] 20 | .filter(Boolean) 21 | .join(' '); 22 | 23 | return ( 24 | // Messages stack from bottom up so the latest appears closest to the input 25 |
26 | {messages.slice(0, MAX_MESSAGES).map((message) => ( 27 | 28 | ))} 29 |
30 | ); 31 | }; 32 | 33 | export default PopupMessageContainer; 34 | -------------------------------------------------------------------------------- /react/components/preferences/AddSelectedItemsOnOpenToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PreferenceToggle from "./PreferenceToggle"; 3 | import { getPref, setPref } from "../../../src/utils/prefs"; 4 | 5 | interface AddSelectedItemsOnOpenToggleProps { 6 | checked: boolean; 7 | onChange: (checked: boolean) => void; 8 | } 9 | 10 | const AddSelectedItemsOnOpenToggle: React.FC = ({ checked, onChange }) => { 11 | const handleChange = (newValue: boolean) => { 12 | setPref("addSelectedItemsOnOpen", newValue); 13 | onChange(newValue); 14 | }; 15 | 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | export default AddSelectedItemsOnOpenToggle; -------------------------------------------------------------------------------- /react/components/icons/UserIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type UserIconProps = React.SVGProps; 4 | 5 | const UserIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default UserIcon; -------------------------------------------------------------------------------- /react/components/preferences/AddSelectedItemsOnNewThreadToggle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PreferenceToggle from "./PreferenceToggle"; 3 | import { getPref, setPref } from "../../../src/utils/prefs"; 4 | 5 | interface AddSelectedItemsOnNewThreadToggleProps { 6 | checked: boolean; 7 | onChange: (checked: boolean) => void; 8 | } 9 | 10 | const AddSelectedItemsOnNewThreadToggle: React.FC = ({ checked, onChange }) => { 11 | const handleChange = (newValue: boolean) => { 12 | setPref("addSelectedItemsOnNewThread", newValue); 13 | onChange(newValue); 14 | }; 15 | 16 | return ( 17 | 24 | ); 25 | }; 26 | 27 | export default AddSelectedItemsOnNewThreadToggle; -------------------------------------------------------------------------------- /react/utils/openPDFInNewWindow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Open a PDF attachment in a new window 3 | * @param {Zotero.Item} item - The Zotero Item object (must be a PDF attachment or have a PDF attachment) 4 | */ 5 | export async function openPDFInNewWindow(item: Zotero.Item, page: number | null = null) { 6 | // If the item itself is not a PDF attachment, find the first available PDF attachment 7 | let pdfItem: Zotero.Item | null = item; 8 | if (!item.isPDFAttachment()) { 9 | // Get all attachments and find the first PDF 10 | pdfItem = await item.getBestAttachment() || null; 11 | if (!pdfItem) { 12 | // No PDF attachment found 13 | return false; 14 | } 15 | } 16 | 17 | // Open the PDF in a new window 18 | await Zotero.Reader.open( 19 | pdfItem.id, // The item ID 20 | // @ts-ignore null is a valid location 21 | page, // Location (null to open at the first page) 22 | { 23 | openInWindow: true, // open in a new window 24 | allowDuplicate: true // allow opening multiple windows of the same PDF 25 | } 26 | ); 27 | 28 | return true; 29 | } -------------------------------------------------------------------------------- /react/components/icons/TextAlignLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type TextAlignLeftIconProps = React.SVGProps; 4 | 5 | const TextAlignLeftIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default TextAlignLeftIcon; 15 | -------------------------------------------------------------------------------- /react/types/popupMessage.ts: -------------------------------------------------------------------------------- 1 | import { FileStatusSummary } from "./fileStatus"; 2 | 3 | export const POPUP_MESSAGE_DURATION = 4000; // 4 seconds 4 | 5 | export type PopupMessageType = 'info' | 'warning' | 'error' | 'plan_change' | 'indexing_complete' | 'version_update' | 'items_summary'; 6 | 7 | export interface PopupMessageFeature { 8 | title: string; 9 | description?: string; 10 | } 11 | 12 | export interface PopupMessage { 13 | id: string; 14 | type: PopupMessageType; 15 | title?: string; 16 | text?: string; 17 | customContent?: React.ReactNode; 18 | icon?: React.ReactNode; 19 | expire?: boolean; // Defaults to true 20 | buttonIcon?: React.ComponentType>; 21 | buttonOnClick?: () => void 22 | fileStatusSummary?: FileStatusSummary; 23 | planName?: string; 24 | showProgress?: boolean; 25 | count?: number; // Count message occurrences 26 | duration?: number; // Duration in milliseconds, defaults to POPUP_MESSAGE_DURATION 27 | showGoToFileStatusButton?: boolean; 28 | showSettingsButton?: boolean; 29 | featureList?: PopupMessageFeature[]; 30 | learnMoreUrl?: string; 31 | learnMoreLabel?: string; 32 | } 33 | -------------------------------------------------------------------------------- /react/components/icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type CopyIconProps = React.SVGProps; 4 | 5 | const CopyIcon: React.FC = (props) => ( 6 | 7 | 8 | 9 | 10 | ); 11 | 12 | export default CopyIcon; -------------------------------------------------------------------------------- /react/utils/stringUtils.ts: -------------------------------------------------------------------------------- 1 | // function to truncate text to a max length 2 | export const truncateText = (text: string, maxLength: number) => { 3 | if (text.length <= maxLength) return text; 4 | return text.slice(0, maxLength) + '...'; 5 | } 6 | 7 | 8 | export function formatNumberRanges(numbers: number[]): string { 9 | if (numbers.length === 0) return ""; 10 | 11 | // Sort and remove duplicates just in case 12 | const sorted = [...new Set(numbers)].sort((a, b) => a - b); 13 | 14 | const parts: string[] = []; 15 | let start = sorted[0]; 16 | let prev = sorted[0]; 17 | 18 | for (let i = 1; i <= sorted.length; i++) { 19 | const current = sorted[i]; 20 | if (current === prev + 1) { 21 | // Still in a range, keep going 22 | prev = current; 23 | } else { 24 | // End of a range 25 | if (start === prev) { 26 | parts.push(`${start}`); 27 | } else { 28 | parts.push(`${start}-${prev}`); 29 | } 30 | // Start a new range 31 | start = current; 32 | prev = current; 33 | } 34 | } 35 | 36 | return parts.join(", "); 37 | } 38 | -------------------------------------------------------------------------------- /addon/content/beaverWindow.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 21 |