research-agent-ui 0.1.292 β 0.1.294
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 +42 -1
- package/dist/ArticleExtractPanel-25tRTnpl.cjs +2 -0
- package/dist/ArticleExtractPanel-D_y16YHG.js +389 -0
- package/dist/ChatConversationThread-BIJ8aNug.js +1428 -0
- package/dist/ChatConversationThread-BoNH0wvs.cjs +17 -0
- package/dist/DownloadsDialog-CCPj0hA8.cjs +1 -0
- package/dist/{DownloadsDialog-bTEDe2DI.js β DownloadsDialog-DvS23Dh-.js} +8 -9
- package/dist/api/handlers/autocomplete.d.ts +6 -9
- package/dist/api/handlers/openai-transcription.d.ts +53 -0
- package/dist/api/handlers/transcript.d.ts +33 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api.cjs +10 -10
- package/dist/api.mjs +161 -208
- package/dist/app/index.d.ts +1 -1
- package/dist/app/useChunkErrorReload.d.ts +13 -2
- package/dist/components/ArticleReader/ArticleReaderMount.d.ts +5 -0
- package/dist/components/ArticleReader/ExtractPanelContext.d.ts +17 -1
- package/dist/components/ChatConversation/MessageBubble/AssistantMessageActions.d.ts +2 -1
- package/dist/components/ChatConversation/background-art.d.ts +3 -1
- package/dist/components/ChatConversation/background-cache.d.ts +41 -0
- package/dist/components/Footer.d.ts +5 -4
- package/dist/components/Footer.stories.d.ts +3 -3
- package/dist/config.cjs +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.mjs +4 -3
- package/dist/dialog-7GDz0sWx.cjs +1 -0
- package/dist/dialog-Cufn2u50.js +80 -0
- package/dist/hooks/voice/useKokoroTTS.d.ts +2 -0
- package/dist/hooks/voice/useTextToVoice.d.ts +2 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.mjs +4 -2
- package/dist/lib/file-sources.d.ts +8 -0
- package/dist/lib/google-drive-source.d.ts +100 -0
- package/dist/lib/kokoro.d.ts +14 -2
- package/dist/lib/qwksearch-connect.d.ts +79 -0
- package/dist/settings/index.d.ts +15 -0
- package/dist/src-GPr1ceY8.js +6165 -0
- package/dist/src-ebmQu_if.cjs +34 -0
- package/dist/tooltip-Bl3IdX-I.js +179 -0
- package/dist/tooltip-DmUO6ZUz.cjs +1 -0
- package/dist/types/fileSource.d.ts +9 -0
- package/dist/workspace/QwkSearchWorkspaceApp.d.ts +9 -2
- package/dist/workspace/ResearchWorkspaceView.d.ts +18 -4
- package/dist/workspace.cjs +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.mjs +234 -91
- package/package.json +54 -51
- package/src/app/useChunkErrorReload.ts +92 -0
- package/src/settings/index.ts +23 -0
- package/src/settings/search.json +19 -1
- package/dist/DownloadsDialog-Ca6aRRcP.cjs +0 -1
- package/dist/src-HL9JjPc-.js +0 -8218
- package/dist/src-r4BsCIB4.cjs +0 -51
package/README.md
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
</p>
|
|
21
21
|
<!-- template-git-repo:badges:end -->
|
|
22
22
|
|
|
23
|
+
<!-- skills:install:start -->
|
|
24
|
+
**π€ Agent skill** β `npx skills@latest add https://github.com/OpenSourceAGI/qwksearch-research-agent --skill research-agent-ui` ([what it covers](../../skills/research-agent-ui/SKILL.md))
|
|
25
|
+
<!-- skills:install:end -->
|
|
26
|
+
|
|
23
27
|
# research-agent-ui
|
|
24
28
|
|
|
25
29
|
|
|
@@ -70,7 +74,7 @@ import { QwkSearchProviders, openSpotlight } from 'research-agent-ui';
|
|
|
70
74
|
</QwkSearchProviders>;
|
|
71
75
|
|
|
72
76
|
// β¦and from a button somewhere in your own chrome:
|
|
73
|
-
<button onClick={() => openSpotlight()}>Search
|
|
77
|
+
<button onClick={() => openSpotlight()}>Search </button>;
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
Rows that open a chat or a settings section go through `onOpenChat` /
|
|
@@ -296,6 +300,43 @@ export const dynamic = "force-dynamic";
|
|
|
296
300
|
export const POST = handleChatRequest;
|
|
297
301
|
```
|
|
298
302
|
|
|
303
|
+
## Embedding on another site: "Sign in with QwkSearch"
|
|
304
|
+
|
|
305
|
+
A site that embeds the research agent has no QwkSearch session cookie, so by
|
|
306
|
+
default the embed runs as a guest. `createQwkSearchConnectAuthClient` makes the
|
|
307
|
+
login button go through QwkSearch's OAuth-style consent screen
|
|
308
|
+
(`https://qwksearch.com/connect`, authorization code + PKCE) instead. Your
|
|
309
|
+
server finishes the exchange at `POST https://qwksearch.com/api/connect/token`,
|
|
310
|
+
stores the returned API key against your own user, and hands it back to the
|
|
311
|
+
embed; every request the embed then makes to `qwksearch.com/api/` carries it as
|
|
312
|
+
`X-API-Key`, so chats, history and the user's QwkSearch plan work in the embed.
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
import { SessionProvider, createQwkSearchConnectAuthClient } from 'research-agent-ui';
|
|
316
|
+
|
|
317
|
+
const authClient = createQwkSearchConnectAuthClient({
|
|
318
|
+
sessionUrl: '/api/qwksearch/session', // GET -> { connected, user, apiKey, plan, upgradeUrl }
|
|
319
|
+
connectUrl: '/api/qwksearch/connect', // GET -> PKCE + redirect to qwksearch.com/connect
|
|
320
|
+
disconnectUrl: '/api/qwksearch/disconnect' // POST -> forget the stored key
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
<SessionProvider authClient={authClient} enableGoogleOneTap={false}>β¦</SessionProvider>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The flow, from your server's side:
|
|
327
|
+
|
|
328
|
+
1. Make a PKCE verifier, keep it (an httpOnly cookie), and redirect to
|
|
329
|
+
`https://qwksearch.com/connect?redirect_uri=<your callback>&state=<random>&code_challenge=<S256>&code_challenge_method=S256`.
|
|
330
|
+
2. QwkSearch signs the user in if needed and asks them to allow the link, then
|
|
331
|
+
redirects to `redirect_uri?code=β¦&state=β¦` (or `?error=access_denied`).
|
|
332
|
+
3. `POST /api/connect/token` with `{ code, redirect_uri, code_verifier }` β
|
|
333
|
+
`{ api_key, user, plan, upgrade_url }`.
|
|
334
|
+
4. `GET /api/connect/me` with the key as `X-API-Key` refreshes the profile and
|
|
335
|
+
plan later β e.g. after the user follows `upgrade_url` to upgrade.
|
|
336
|
+
|
|
337
|
+
`redirect_uri` must be on QwkSearch's allowlist (debate-ai.com, ebate.app and localhost
|
|
338
|
+
by default; more via the `QWKSEARCH_CONNECT_ORIGINS` env var on qwksearch-web).
|
|
339
|
+
|
|
299
340
|
## Configuration
|
|
300
341
|
|
|
301
342
|
`configureResearchAgentUI` overrides app-specific values (branding strings,
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./rolldown-runtime-BocRIvOZ.cjs"),t=require("./dialog-7GDz0sWx.cjs"),n=require("./tooltip-DmUO6ZUz.cjs"),r=require("./src-ebmQu_if.cjs"),i=require("./config.cjs");let a=require("react");a=e.t(a,1);let o=require("react/jsx-runtime"),s=require("sonner"),c=require("qwksearch-api-client");var l=a.forwardRef(({className:e,...n},r)=>(0,o.jsx)(`span`,{ref:r,className:t.o(`absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0`,e),style:{clip:`rect(0, 0, 0, 0)`,clipPath:`inset(50%)`},...n}));l.displayName=`VisuallyHidden`;function u(e){return e instanceof Error&&e.name===`AbortError`}async function ee(e,t){if(t.share)try{return await t.share(e),`shared`}catch(e){if(u(e))return`cancelled`}return await t.writeText(e.url),`copied`}var d=e=>{let{isOpen:u,url:d,searchText:te,suggestQuestions:f,panelWidth:ne,setPanelWidth:p,closePanel:re}=n.a(),{isAuthenticated:ie}=n.c(),{chatModelProvider:m}=n.u(),h=e.isOpen===void 0?u:e.isOpen,g=e.onClose||re,_=e.url||d,ae=e.searchText===void 0?te:e.searchText,[v,y]=(0,a.useState)(null),[b,oe]=(0,a.useState)([]),[se,x]=(0,a.useState)(!1),[ce,S]=(0,a.useState)(!1),[C,w]=(0,a.useState)(i.researchAgentUIConfig.defaultSummarizePrompt),[le,T]=(0,a.useState)(!1),[E,ue]=(0,a.useState)(!1),[D,de]=(0,a.useState)(!1),[O,fe]=(0,a.useState)(``),[k,pe]=(0,a.useState)(``),[me,A]=(0,a.useState)([]),[he,ge]=(0,a.useState)(``),[j,M]=(0,a.useState)(!1),[_e,N]=(0,a.useState)(!1),[P,F]=(0,a.useState)(!1),[I,ve]=(0,a.useState)(!1),[ye,be]=(0,a.useState)(ne),[L,R]=(0,a.useState)(!1),[xe,z]=(0,a.useState)(!1),[B,V]=(0,a.useState)(1),Se=(0,a.useRef)(null),H=(0,a.useRef)({}),U=.5,W=1.8,G=.1;(0,a.useEffect)(()=>{let e=parseFloat(localStorage.getItem(`articleFontScale`)||``);Number.isNaN(e)||V(Math.min(W,Math.max(U,e)))},[]);let K=e=>{let t=Math.min(W,Math.max(U,Math.round(e*100)/100));V(t);try{localStorage.setItem(`articleFontScale`,String(t))}catch(e){console.error(`Error storing article font scale:`,e)}},q=()=>K(B+G),J=()=>K(B-G),Y=()=>K(1);(0,a.useEffect)(()=>{let e=()=>{ve(window.innerWidth>=1024)};return e(),window.addEventListener(`resize`,e),()=>window.removeEventListener(`resize`,e)},[]),(0,a.useEffect)(()=>{let e=e=>{if(!L)return;let t=window.innerWidth-e.clientX,n=Math.max(400,Math.min(t,window.innerWidth-100));be(n),p(n)},t=()=>{R(!1)};return L&&(document.addEventListener(`mousemove`,e),document.addEventListener(`mouseup`,t),document.body.style.cursor=`ew-resize`,document.body.style.userSelect=`none`),()=>{document.removeEventListener(`mousemove`,e),document.removeEventListener(`mouseup`,t),document.body.style.cursor=``,document.body.style.userSelect=``}},[L,p]),(0,a.useEffect)(()=>{h&&_?(z(!1),Ce()):h||z(!1)},[h,_]);let Ce=async()=>{console.log(`[ArticleExtractPanel] Extracting URL:`,_);try{let e=new URL(_);if([/^https?:\/\/(www\.)?google\.[^/]+\/search/i,/^https?:\/\/(www\.)?bing\.com\/search/i,/^https?:\/\/(www\.)?duckduckgo\.com\/\?/i].some(e=>e.test(_))){s.toast.error(`Cannot extract articles from search result pages`),T(!1),z(!0);return}(e.pathname===`/`||e.pathname===``)&&(console.error(`[ArticleExtractPanel] URL is domain-only, missing article path:`,_),s.toast.error(`Invalid article URL - only domain provided`))}catch(e){console.error(`[ArticleExtractPanel] Invalid URL format:`,_,e)}T(!0);try{let{data:e}=await(0,c.getArticle)({query:{url:_}}),t=e?.article,n=t?.html?.replace(/<[^>]*>/g,``).trim()||``;t?.error||n.length<20?(document.dispatchEvent(new CustomEvent(`onInvokeChromeAPI`,{detail:{type:`extractURL`,url:_}})),await Promise.race([new Promise(e=>{let t=async n=>{window.removeEventListener(`onExtractionResult`,t);let{data:r}=await(0,c.getArticle)({query:{url:_}}),i=r?.article;i&&(y(i),i.followUpQuestions?.length>0?A(i.followUpQuestions):f&&(A([]),Z(`suggest-followups`,{source:i}))),e()};window.addEventListener(`onExtractionResult`,t)}),new Promise(e=>setTimeout(e,1e4))])):(y(t),t?.followUpQuestions&&t.followUpQuestions.length>0?A(t.followUpQuestions):f&&t&&(A([]),Z(`suggest-followups`,{source:t}))),ie&&we()}catch(e){console.error(`[ArticleExtractPanel] Error extracting URL:`,e),s.toast.error(`Failed to load article`)}finally{T(!1),z(!0)}},we=async()=>{try{let{data:e}=await(0,c.listFavorites)(),t=e?.favorites?.some(e=>e.url===_);M(!!t)}catch(e){console.error(`Error checking favorite status:`,e)}},X=async()=>{if(v){N(!0);try{j?(await(0,c.removeFavorite)({query:{url:_}}),M(!1),s.toast.info(`Removed from favorites`)):(await(0,c.addFavorite)({body:{url:v.url||_,title:v.title,cite:v.cite,author:v.author,author_cite:v.author_cite,date:v.date,source:v.source,word_count:v.word_count,html:v.html}}),M(!0),s.toast.success(`Added to favorites`))}catch(e){console.error(`Error toggling favorite:`,e),s.toast.error(`Failed to toggle favorite`)}finally{N(!1)}}},Te=()=>{if(m?.key&&m?.providerId)return m;try{let e=localStorage.getItem(`chatModelKey`)||``,t=localStorage.getItem(`chatModelProviderId`)||``;if(e&&t)return{key:e,providerId:t}}catch(e){console.error(`Error reading persisted chat model:`,e)}return m},Z=async(e,{source:t=v,question:n=C}={})=>{if(!t)return;let r=Te(),a=t.html?.replace(/<[^>]*>?/g,``).slice(0,i.researchAgentUIConfig.maxArticleLength),o=e===`question`,s=o?ue:de,l=o?pe:ge;s(!0),l(``);try{if(o){let e=[ae,n].filter(Boolean).join(`
|
|
2
|
+
`);if(!a||a.length===0)throw Error(`Article content is empty`);let{data:t,error:i}=await(0,c.articleQa)({body:{article:a,question:e,chatHistory:b.slice(-5),chatModel:r}});if(i){let e=i.details||i.error||i.message||`Article QA failed`;throw Error(e)}let o=t?.content||``;fe(o),oe(e=>[...e,{role:`user`,content:n,time:new Date().toISOString()},{role:`assistant`,content:o,time:new Date().toISOString()}]);try{await(0,c.updateArticle)({body:{url:_,question:n,answer:o}})}catch(e){console.error(`Error storing Q&A in cache:`,e)}}else{let e=parseInt(localStorage.getItem(`maxFollowupQuestions`)||`4`);if(!a||a.length===0)throw Error(`Article content is empty`);let{data:t,error:n}=await(0,c.articleFollowups)({body:{article:a,chatHistory:b.slice(-5),maxQuestions:e,chatModel:r}});if(n){let e=n.details||n.error||n.message||`Article followups failed`;throw Error(e)}let i=t?.extract||[];A(i);try{await(0,c.updateArticle)({body:{url:_,followUpQuestions:i}})}catch(e){console.error(`Error storing follow-up questions in cache:`,e)}}}catch(e){console.error(`Error calling language API:`,e),l(e instanceof Error||typeof e==`object`&&e&&`message`in e?e.message:`Failed to get AI response`)}finally{s(!1)}},Ee=e=>{w(e),Z(`question`,{question:e})},Q=async()=>{if(!v)return;let e=`${O}\n\n\n${v.cite||``}\n\n\n${v.html}`;try{await navigator.clipboard.writeText(e),x(!0),setTimeout(()=>x(!1),2e3)}catch(e){console.error(`Failed to copy to clipboard:`,e)}},De=async()=>{let e=v?.url||_;if(e)try{await ee({title:v?.title,text:v?.cite,url:e},{share:typeof navigator<`u`&&navigator.share?e=>navigator.share(e):void 0,writeText:e=>navigator.clipboard.writeText(e)})===`copied`&&(S(!0),setTimeout(()=>S(!1),2e3))}catch(e){console.error(`Failed to share article:`,e)}};H.current={ask:()=>Z(`question`),suggest:()=>Z(`suggest-followups`),copy:Q,highlight:()=>F(e=>!e),favorite:X,open:()=>{let e=v?.url||_;e&&window.open(e,`_blank`,`noopener,noreferrer`)},zoomIn:q,zoomOut:J,zoomReset:Y,close:g},(0,a.useEffect)(()=>{if(!h)return;let e=e=>/^[a-z]$/.test(e)?`Key${e.toUpperCase()}`:/^[0-9]$/.test(e)?`Digit${e}`:e===`-`?`Minus`:e===`=`?`Equal`:e,t={};Object.keys(r.D).forEach(n=>{let{alt:i,key:a}=r.D[n];i&&(t[e(a.toLowerCase())]=n)});let n=e=>{if(e.key===`Escape`){H.current.close?.();return}if(!e.altKey||e.ctrlKey||e.metaKey)return;let n=e.target;if(n&&(n.tagName===`INPUT`||n.tagName===`TEXTAREA`||n.isContentEditable))return;let r=t[e.code];if(!r)return;let i=H.current[r];i&&(e.preventDefault(),i())};return window.addEventListener(`keydown`,n),()=>window.removeEventListener(`keydown`,n)},[h]);let $=()=>(0,o.jsxs)(`div`,{className:`flex h-full flex-col bg-background shadow-xl`,children:[(0,o.jsx)(`div`,{className:`shrink-0 border-b border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80`,children:(0,o.jsx)(r.O,{isLoadingAI:E,isLoadingFollowups:D,isLoadingFavorite:_e,isFavorited:j,isHighlightMode:P,articleUrl:v?.url||_,fontScale:B,onAskClick:()=>Z(`question`),onSuggestClick:()=>Z(`suggest-followups`),onCopyClick:Q,onShareClick:De,onFavoriteClick:X,onHighlightToggle:()=>F(!P),onZoomIn:q,onZoomOut:J,onZoomReset:Y,onClose:g})}),(0,o.jsx)(`div`,{className:`flex-1 overflow-y-auto`,children:(0,o.jsxs)(`div`,{className:`p-4 space-y-6`,children:[(0,o.jsxs)(`div`,{className:`space-y-4`,children:[se&&(0,o.jsx)(`div`,{className:`bg-blue-500 text-white text-sm font-medium px-3 py-2 rounded-md shadow-lg`,children:`Copied!`}),ce&&(0,o.jsx)(`div`,{className:`bg-blue-500 text-white text-sm font-medium px-3 py-2 rounded-md shadow-lg`,children:`Link copied!`}),(0,o.jsx)(r.E,{value:C,onChange:w,onSubmit:()=>Z(`question`)}),(0,o.jsx)(r.T,{questions:me,isLoading:D,error:he,onQuestionClick:Ee}),b.length>0&&(0,o.jsx)(`div`,{className:`space-y-4`,children:b.map((e,t)=>(0,o.jsx)(`div`,{className:`space-y-2`,children:e.role===`user`?(0,o.jsxs)(`div`,{className:`bg-primary/10 border border-primary/20 rounded-lg p-3`,children:[(0,o.jsx)(`div`,{className:`text-xs font-semibold text-primary mb-1`,children:`Your Question`}),(0,o.jsx)(`div`,{className:`text-sm text-foreground`,children:e.content})]}):(0,o.jsxs)(`div`,{className:`bg-muted rounded-lg shadow-md p-4`,children:[(0,o.jsx)(`div`,{className:`text-xs font-semibold text-muted-foreground mb-2`,children:`AI Response`}),(0,o.jsx)(`div`,{className:`text-sm`,dangerouslySetInnerHTML:{__html:e.content}})]})},t))}),(E||O&&b.length===0)&&(0,o.jsx)(r.w,{response:O,isLoading:E,error:k}),k&&!E&&(0,o.jsx)(`div`,{className:`bg-red-500 text-white p-2 rounded-md text-sm`,children:k})]}),v&&(0,o.jsx)(r.S,{article:v,isHighlightMode:P,fontScale:B})]})})]});return!h||!xe?null:I?(0,o.jsxs)(`div`,{className:`fixed top-0 right-0 h-screen z-40 transition-all duration-300`,style:{width:`${ye}px`},children:[(0,o.jsx)(`div`,{ref:Se,onMouseDown:()=>R(!0),className:`absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize hover:bg-primary bg-transparent transition-colors z-50`,style:{touchAction:`none`},children:(0,o.jsx)(`div`,{className:`absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-12 rounded-full bg-muted-foreground opacity-50 hover:opacity-100 transition-opacity`})}),$()]}):(0,o.jsx)(t.t,{open:h,onOpenChange:e=>!e&&g(),children:(0,o.jsxs)(t.n,{className:`pointer-events-auto fixed inset-0 !top-0 !left-0 !translate-x-0 !translate-y-0 h-full w-full max-w-none p-0 m-0 rounded-none border-0 overflow-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right`,hideCloseButton:!0,children:[(0,o.jsx)(l,{children:(0,o.jsx)(t.i,{children:`Article Details`})}),(0,o.jsx)(`div`,{className:`h-full overflow-y-auto`,children:$()})]})})};exports.default=d;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { i as e, n as t, o as n, t as r } from "./dialog-Cufn2u50.js";
|
|
2
|
+
import { a as i, c as a, u as o } from "./tooltip-Bl3IdX-I.js";
|
|
3
|
+
import { D as s, E as ee, O as te, S as ne, T as re, w as ie } from "./src-GPr1ceY8.js";
|
|
4
|
+
import { researchAgentUIConfig as ae } from "./config.mjs";
|
|
5
|
+
import * as c from "react";
|
|
6
|
+
import { useEffect as l, useRef as u, useState as d } from "react";
|
|
7
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
8
|
+
import { toast as m } from "sonner";
|
|
9
|
+
import { addFavorite as oe, articleFollowups as se, articleQa as ce, getArticle as h, listFavorites as le, removeFavorite as ue, updateArticle as g } from "qwksearch-api-client";
|
|
10
|
+
// src/ui/visually-hidden.tsx
|
|
11
|
+
var _ = c.forwardRef(({ className: e, ...t }, r) => /* @__PURE__ */ f("span", {
|
|
12
|
+
ref: r,
|
|
13
|
+
className: n("absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0", e),
|
|
14
|
+
style: {
|
|
15
|
+
clip: "rect(0, 0, 0, 0)",
|
|
16
|
+
clipPath: "inset(50%)"
|
|
17
|
+
},
|
|
18
|
+
...t
|
|
19
|
+
}));
|
|
20
|
+
_.displayName = "VisuallyHidden";
|
|
21
|
+
// src/lib/shareArticle.ts
|
|
22
|
+
function v(e) {
|
|
23
|
+
return e instanceof Error && e.name === "AbortError";
|
|
24
|
+
}
|
|
25
|
+
async function de(e, t) {
|
|
26
|
+
if (t.share) try {
|
|
27
|
+
return await t.share(e), "shared";
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (v(e)) return "cancelled";
|
|
30
|
+
}
|
|
31
|
+
return await t.writeText(e.url), "copied";
|
|
32
|
+
}
|
|
33
|
+
// src/components/ArticleReader/ArticleExtractPanel.tsx
|
|
34
|
+
var y = (n) => {
|
|
35
|
+
let { isOpen: c, url: v, searchText: y, suggestQuestions: fe, panelWidth: pe, setPanelWidth: b, closePanel: me } = i(), { isAuthenticated: he } = a(), { chatModelProvider: x } = o(), S = n.isOpen === void 0 ? c : n.isOpen, C = n.onClose || me, w = n.url || v, ge = n.searchText === void 0 ? y : n.searchText, [T, E] = d(null), [D, _e] = d([]), [ve, O] = d(!1), [ye, k] = d(!1), [A, j] = d(ae.defaultSummarizePrompt), [be, M] = d(!1), [N, xe] = d(!1), [P, Se] = d(!1), [F, Ce] = d(""), [I, we] = d(""), [Te, L] = d([]), [Ee, De] = d(""), [R, z] = d(!1), [Oe, B] = d(!1), [V, H] = d(!1), [ke, Ae] = d(!1), [je, Me] = d(pe), [U, W] = d(!1), [Ne, G] = d(!1), [K, q] = d(1), Pe = u(null), J = u({}), Y = .5, X = 1.8, Z = .1;
|
|
36
|
+
l(() => {
|
|
37
|
+
let e = parseFloat(localStorage.getItem("articleFontScale") || "");
|
|
38
|
+
Number.isNaN(e) || q(Math.min(X, Math.max(Y, e)));
|
|
39
|
+
}, []);
|
|
40
|
+
let Q = (e) => {
|
|
41
|
+
let t = Math.min(X, Math.max(Y, Math.round(e * 100) / 100));
|
|
42
|
+
q(t);
|
|
43
|
+
try {
|
|
44
|
+
localStorage.setItem("articleFontScale", String(t));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error("Error storing article font scale:", e);
|
|
47
|
+
}
|
|
48
|
+
}, Fe = () => Q(K + Z), Ie = () => Q(K - Z), Le = () => Q(1);
|
|
49
|
+
l(() => {
|
|
50
|
+
let e = () => {
|
|
51
|
+
Ae(window.innerWidth >= 1024);
|
|
52
|
+
};
|
|
53
|
+
return e(), window.addEventListener("resize", e), () => window.removeEventListener("resize", e);
|
|
54
|
+
}, []), l(() => {
|
|
55
|
+
let e = (e) => {
|
|
56
|
+
if (!U) return;
|
|
57
|
+
let t = window.innerWidth - e.clientX, n = Math.max(400, Math.min(t, window.innerWidth - 100));
|
|
58
|
+
Me(n), b(n);
|
|
59
|
+
}, t = () => {
|
|
60
|
+
W(!1);
|
|
61
|
+
};
|
|
62
|
+
return U && (document.addEventListener("mousemove", e), document.addEventListener("mouseup", t), document.body.style.cursor = "ew-resize", document.body.style.userSelect = "none"), () => {
|
|
63
|
+
document.removeEventListener("mousemove", e), document.removeEventListener("mouseup", t), document.body.style.cursor = "", document.body.style.userSelect = "";
|
|
64
|
+
};
|
|
65
|
+
}, [U, b]), l(() => {
|
|
66
|
+
S && w ? (G(!1), Re()) : S || G(!1);
|
|
67
|
+
}, [S, w]);
|
|
68
|
+
let Re = async () => {
|
|
69
|
+
console.log("[ArticleExtractPanel] Extracting URL:", w);
|
|
70
|
+
try {
|
|
71
|
+
let e = new URL(w);
|
|
72
|
+
if ([
|
|
73
|
+
/^https?:\/\/(www\.)?google\.[^/]+\/search/i,
|
|
74
|
+
/^https?:\/\/(www\.)?bing\.com\/search/i,
|
|
75
|
+
/^https?:\/\/(www\.)?duckduckgo\.com\/\?/i
|
|
76
|
+
].some((e) => e.test(w))) {
|
|
77
|
+
m.error("Cannot extract articles from search result pages"), M(!1), G(!0);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
(e.pathname === "/" || e.pathname === "") && (console.error("[ArticleExtractPanel] URL is domain-only, missing article path:", w), m.error("Invalid article URL - only domain provided"));
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.error("[ArticleExtractPanel] Invalid URL format:", w, e);
|
|
83
|
+
}
|
|
84
|
+
M(!0);
|
|
85
|
+
try {
|
|
86
|
+
let { data: e } = await h({ query: { url: w } }), t = e?.article, n = t?.html?.replace(/<[^>]*>/g, "").trim() || "";
|
|
87
|
+
t?.error || n.length < 20 ? (document.dispatchEvent(new CustomEvent("onInvokeChromeAPI", { detail: {
|
|
88
|
+
type: "extractURL",
|
|
89
|
+
url: w
|
|
90
|
+
} })), await Promise.race([new Promise((e) => {
|
|
91
|
+
let t = async (n) => {
|
|
92
|
+
window.removeEventListener("onExtractionResult", t);
|
|
93
|
+
let { data: r } = await h({ query: { url: w } }), i = r?.article;
|
|
94
|
+
i && (E(i), i.followUpQuestions?.length > 0 ? L(i.followUpQuestions) : fe && (L([]), $("suggest-followups", { source: i }))), e();
|
|
95
|
+
};
|
|
96
|
+
window.addEventListener("onExtractionResult", t);
|
|
97
|
+
}), new Promise((e) => setTimeout(e, 1e4))])) : (E(t), t?.followUpQuestions && t.followUpQuestions.length > 0 ? L(t.followUpQuestions) : fe && t && (L([]), $("suggest-followups", { source: t }))), he && ze();
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error("[ArticleExtractPanel] Error extracting URL:", e), m.error("Failed to load article");
|
|
100
|
+
} finally {
|
|
101
|
+
M(!1), G(!0);
|
|
102
|
+
}
|
|
103
|
+
}, ze = async () => {
|
|
104
|
+
try {
|
|
105
|
+
let { data: e } = await le(), t = e?.favorites?.some((e) => e.url === w);
|
|
106
|
+
z(!!t);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.error("Error checking favorite status:", e);
|
|
109
|
+
}
|
|
110
|
+
}, Be = async () => {
|
|
111
|
+
if (T) {
|
|
112
|
+
B(!0);
|
|
113
|
+
try {
|
|
114
|
+
R ? (await ue({ query: { url: w } }), z(!1), m.info("Removed from favorites")) : (await oe({ body: {
|
|
115
|
+
url: T.url || w,
|
|
116
|
+
title: T.title,
|
|
117
|
+
cite: T.cite,
|
|
118
|
+
author: T.author,
|
|
119
|
+
author_cite: T.author_cite,
|
|
120
|
+
date: T.date,
|
|
121
|
+
source: T.source,
|
|
122
|
+
word_count: T.word_count,
|
|
123
|
+
html: T.html
|
|
124
|
+
} }), z(!0), m.success("Added to favorites"));
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error("Error toggling favorite:", e), m.error("Failed to toggle favorite");
|
|
127
|
+
} finally {
|
|
128
|
+
B(!1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, Ve = () => {
|
|
132
|
+
if (x?.key && x?.providerId) return x;
|
|
133
|
+
try {
|
|
134
|
+
let e = localStorage.getItem("chatModelKey") || "", t = localStorage.getItem("chatModelProviderId") || "";
|
|
135
|
+
if (e && t) return {
|
|
136
|
+
key: e,
|
|
137
|
+
providerId: t
|
|
138
|
+
};
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error("Error reading persisted chat model:", e);
|
|
141
|
+
}
|
|
142
|
+
return x;
|
|
143
|
+
}, $ = async (e, { source: t = T, question: n = A } = {}) => {
|
|
144
|
+
if (!t) return;
|
|
145
|
+
let r = Ve(), i = t.html?.replace(/<[^>]*>?/g, "").slice(0, ae.maxArticleLength), a = e === "question", o = a ? xe : Se, s = a ? we : De;
|
|
146
|
+
o(!0), s("");
|
|
147
|
+
try {
|
|
148
|
+
if (a) {
|
|
149
|
+
let e = [ge, n].filter(Boolean).join("\n");
|
|
150
|
+
if (!i || i.length === 0) throw Error("Article content is empty");
|
|
151
|
+
let { data: t, error: a } = await ce({ body: {
|
|
152
|
+
article: i,
|
|
153
|
+
question: e,
|
|
154
|
+
chatHistory: D.slice(-5),
|
|
155
|
+
chatModel: r
|
|
156
|
+
} });
|
|
157
|
+
if (a) {
|
|
158
|
+
let e = a.details || a.error || a.message || "Article QA failed";
|
|
159
|
+
throw Error(e);
|
|
160
|
+
}
|
|
161
|
+
let o = t?.content || "";
|
|
162
|
+
Ce(o), _e((e) => [
|
|
163
|
+
...e,
|
|
164
|
+
{
|
|
165
|
+
role: "user",
|
|
166
|
+
content: n,
|
|
167
|
+
time: (/* @__PURE__ */ new Date()).toISOString()
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
role: "assistant",
|
|
171
|
+
content: o,
|
|
172
|
+
time: (/* @__PURE__ */ new Date()).toISOString()
|
|
173
|
+
}
|
|
174
|
+
]);
|
|
175
|
+
try {
|
|
176
|
+
await g({ body: {
|
|
177
|
+
url: w,
|
|
178
|
+
question: n,
|
|
179
|
+
answer: o
|
|
180
|
+
} });
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error("Error storing Q&A in cache:", e);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
let e = parseInt(localStorage.getItem("maxFollowupQuestions") || "4");
|
|
186
|
+
if (!i || i.length === 0) throw Error("Article content is empty");
|
|
187
|
+
let { data: t, error: n } = await se({ body: {
|
|
188
|
+
article: i,
|
|
189
|
+
chatHistory: D.slice(-5),
|
|
190
|
+
maxQuestions: e,
|
|
191
|
+
chatModel: r
|
|
192
|
+
} });
|
|
193
|
+
if (n) {
|
|
194
|
+
let e = n.details || n.error || n.message || "Article followups failed";
|
|
195
|
+
throw Error(e);
|
|
196
|
+
}
|
|
197
|
+
let a = t?.extract || [];
|
|
198
|
+
L(a);
|
|
199
|
+
try {
|
|
200
|
+
await g({ body: {
|
|
201
|
+
url: w,
|
|
202
|
+
followUpQuestions: a
|
|
203
|
+
} });
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error("Error storing follow-up questions in cache:", e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.error("Error calling language API:", e), s(e instanceof Error || typeof e == "object" && e && "message" in e ? e.message : "Failed to get AI response");
|
|
210
|
+
} finally {
|
|
211
|
+
o(!1);
|
|
212
|
+
}
|
|
213
|
+
}, He = (e) => {
|
|
214
|
+
j(e), $("question", { question: e });
|
|
215
|
+
}, Ue = async () => {
|
|
216
|
+
if (!T) return;
|
|
217
|
+
let e = `${F}\n\n\n${T.cite || ""}\n\n\n${T.html}`;
|
|
218
|
+
try {
|
|
219
|
+
await navigator.clipboard.writeText(e), O(!0), setTimeout(() => O(!1), 2e3);
|
|
220
|
+
} catch (e) {
|
|
221
|
+
console.error("Failed to copy to clipboard:", e);
|
|
222
|
+
}
|
|
223
|
+
}, We = async () => {
|
|
224
|
+
let e = T?.url || w;
|
|
225
|
+
if (e) try {
|
|
226
|
+
await de({
|
|
227
|
+
title: T?.title,
|
|
228
|
+
text: T?.cite,
|
|
229
|
+
url: e
|
|
230
|
+
}, {
|
|
231
|
+
share: typeof navigator < "u" && navigator.share ? (e) => navigator.share(e) : void 0,
|
|
232
|
+
writeText: (e) => navigator.clipboard.writeText(e)
|
|
233
|
+
}) === "copied" && (k(!0), setTimeout(() => k(!1), 2e3));
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error("Failed to share article:", e);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
J.current = {
|
|
239
|
+
ask: () => $("question"),
|
|
240
|
+
suggest: () => $("suggest-followups"),
|
|
241
|
+
copy: Ue,
|
|
242
|
+
highlight: () => H((e) => !e),
|
|
243
|
+
favorite: Be,
|
|
244
|
+
open: () => {
|
|
245
|
+
let e = T?.url || w;
|
|
246
|
+
e && window.open(e, "_blank", "noopener,noreferrer");
|
|
247
|
+
},
|
|
248
|
+
zoomIn: Fe,
|
|
249
|
+
zoomOut: Ie,
|
|
250
|
+
zoomReset: Le,
|
|
251
|
+
close: C
|
|
252
|
+
}, l(() => {
|
|
253
|
+
if (!S) return;
|
|
254
|
+
let e = (e) => /^[a-z]$/.test(e) ? `Key${e.toUpperCase()}` : /^[0-9]$/.test(e) ? `Digit${e}` : e === "-" ? "Minus" : e === "=" ? "Equal" : e, t = {};
|
|
255
|
+
Object.keys(s).forEach((n) => {
|
|
256
|
+
let { alt: r, key: i } = s[n];
|
|
257
|
+
r && (t[e(i.toLowerCase())] = n);
|
|
258
|
+
});
|
|
259
|
+
let n = (e) => {
|
|
260
|
+
if (e.key === "Escape") {
|
|
261
|
+
J.current.close?.();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!e.altKey || e.ctrlKey || e.metaKey) return;
|
|
265
|
+
let n = e.target;
|
|
266
|
+
if (n && (n.tagName === "INPUT" || n.tagName === "TEXTAREA" || n.isContentEditable)) return;
|
|
267
|
+
let r = t[e.code];
|
|
268
|
+
if (!r) return;
|
|
269
|
+
let i = J.current[r];
|
|
270
|
+
i && (e.preventDefault(), i());
|
|
271
|
+
};
|
|
272
|
+
return window.addEventListener("keydown", n), () => window.removeEventListener("keydown", n);
|
|
273
|
+
}, [S]);
|
|
274
|
+
let Ge = () => /* @__PURE__ */ p("div", {
|
|
275
|
+
className: "flex h-full flex-col bg-background shadow-xl",
|
|
276
|
+
children: [/* @__PURE__ */ f("div", {
|
|
277
|
+
className: "shrink-0 border-b border-border bg-background/95 px-4 py-3 backdrop-blur supports-[backdrop-filter]:bg-background/80",
|
|
278
|
+
children: /* @__PURE__ */ f(te, {
|
|
279
|
+
isLoadingAI: N,
|
|
280
|
+
isLoadingFollowups: P,
|
|
281
|
+
isLoadingFavorite: Oe,
|
|
282
|
+
isFavorited: R,
|
|
283
|
+
isHighlightMode: V,
|
|
284
|
+
articleUrl: T?.url || w,
|
|
285
|
+
fontScale: K,
|
|
286
|
+
onAskClick: () => $("question"),
|
|
287
|
+
onSuggestClick: () => $("suggest-followups"),
|
|
288
|
+
onCopyClick: Ue,
|
|
289
|
+
onShareClick: We,
|
|
290
|
+
onFavoriteClick: Be,
|
|
291
|
+
onHighlightToggle: () => H(!V),
|
|
292
|
+
onZoomIn: Fe,
|
|
293
|
+
onZoomOut: Ie,
|
|
294
|
+
onZoomReset: Le,
|
|
295
|
+
onClose: C
|
|
296
|
+
})
|
|
297
|
+
}), /* @__PURE__ */ f("div", {
|
|
298
|
+
className: "flex-1 overflow-y-auto",
|
|
299
|
+
children: /* @__PURE__ */ p("div", {
|
|
300
|
+
className: "p-4 space-y-6",
|
|
301
|
+
children: [/* @__PURE__ */ p("div", {
|
|
302
|
+
className: "space-y-4",
|
|
303
|
+
children: [
|
|
304
|
+
ve && /* @__PURE__ */ f("div", {
|
|
305
|
+
className: "bg-blue-500 text-white text-sm font-medium px-3 py-2 rounded-md shadow-lg",
|
|
306
|
+
children: "Copied!"
|
|
307
|
+
}),
|
|
308
|
+
ye && /* @__PURE__ */ f("div", {
|
|
309
|
+
className: "bg-blue-500 text-white text-sm font-medium px-3 py-2 rounded-md shadow-lg",
|
|
310
|
+
children: "Link copied!"
|
|
311
|
+
}),
|
|
312
|
+
/* @__PURE__ */ f(ee, {
|
|
313
|
+
value: A,
|
|
314
|
+
onChange: j,
|
|
315
|
+
onSubmit: () => $("question")
|
|
316
|
+
}),
|
|
317
|
+
/* @__PURE__ */ f(re, {
|
|
318
|
+
questions: Te,
|
|
319
|
+
isLoading: P,
|
|
320
|
+
error: Ee,
|
|
321
|
+
onQuestionClick: He
|
|
322
|
+
}),
|
|
323
|
+
D.length > 0 && /* @__PURE__ */ f("div", {
|
|
324
|
+
className: "space-y-4",
|
|
325
|
+
children: D.map((e, t) => /* @__PURE__ */ f("div", {
|
|
326
|
+
className: "space-y-2",
|
|
327
|
+
children: e.role === "user" ? /* @__PURE__ */ p("div", {
|
|
328
|
+
className: "bg-primary/10 border border-primary/20 rounded-lg p-3",
|
|
329
|
+
children: [/* @__PURE__ */ f("div", {
|
|
330
|
+
className: "text-xs font-semibold text-primary mb-1",
|
|
331
|
+
children: "Your Question"
|
|
332
|
+
}), /* @__PURE__ */ f("div", {
|
|
333
|
+
className: "text-sm text-foreground",
|
|
334
|
+
children: e.content
|
|
335
|
+
})]
|
|
336
|
+
}) : /* @__PURE__ */ p("div", {
|
|
337
|
+
className: "bg-muted rounded-lg shadow-md p-4",
|
|
338
|
+
children: [/* @__PURE__ */ f("div", {
|
|
339
|
+
className: "text-xs font-semibold text-muted-foreground mb-2",
|
|
340
|
+
children: "AI Response"
|
|
341
|
+
}), /* @__PURE__ */ f("div", {
|
|
342
|
+
className: "text-sm",
|
|
343
|
+
dangerouslySetInnerHTML: { __html: e.content }
|
|
344
|
+
})]
|
|
345
|
+
})
|
|
346
|
+
}, t))
|
|
347
|
+
}),
|
|
348
|
+
(N || F && D.length === 0) && /* @__PURE__ */ f(ie, {
|
|
349
|
+
response: F,
|
|
350
|
+
isLoading: N,
|
|
351
|
+
error: I
|
|
352
|
+
}),
|
|
353
|
+
I && !N && /* @__PURE__ */ f("div", {
|
|
354
|
+
className: "bg-red-500 text-white p-2 rounded-md text-sm",
|
|
355
|
+
children: I
|
|
356
|
+
})
|
|
357
|
+
]
|
|
358
|
+
}), T && /* @__PURE__ */ f(ne, {
|
|
359
|
+
article: T,
|
|
360
|
+
isHighlightMode: V,
|
|
361
|
+
fontScale: K
|
|
362
|
+
})]
|
|
363
|
+
})
|
|
364
|
+
})]
|
|
365
|
+
});
|
|
366
|
+
return !S || !Ne ? null : ke ? /* @__PURE__ */ p("div", {
|
|
367
|
+
className: "fixed top-0 right-0 h-screen z-40 transition-all duration-300",
|
|
368
|
+
style: { width: `${je}px` },
|
|
369
|
+
children: [/* @__PURE__ */ f("div", {
|
|
370
|
+
ref: Pe,
|
|
371
|
+
onMouseDown: () => W(!0),
|
|
372
|
+
className: "absolute left-0 top-0 bottom-0 w-1 cursor-ew-resize hover:bg-primary bg-transparent transition-colors z-50",
|
|
373
|
+
style: { touchAction: "none" },
|
|
374
|
+
children: /* @__PURE__ */ f("div", { className: "absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-12 rounded-full bg-muted-foreground opacity-50 hover:opacity-100 transition-opacity" })
|
|
375
|
+
}), Ge()]
|
|
376
|
+
}) : /* @__PURE__ */ f(r, {
|
|
377
|
+
open: S,
|
|
378
|
+
onOpenChange: (e) => !e && C(),
|
|
379
|
+
children: /* @__PURE__ */ p(t, {
|
|
380
|
+
className: "pointer-events-auto fixed inset-0 !top-0 !left-0 !translate-x-0 !translate-y-0 h-full w-full max-w-none p-0 m-0 rounded-none border-0 overflow-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
381
|
+
hideCloseButton: !0,
|
|
382
|
+
children: [/* @__PURE__ */ f(_, { children: /* @__PURE__ */ f(e, { children: "Article Details" }) }), /* @__PURE__ */ f("div", {
|
|
383
|
+
className: "h-full overflow-y-auto",
|
|
384
|
+
children: Ge()
|
|
385
|
+
})]
|
|
386
|
+
})
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
export { y as default };
|