@veluai/velu 0.2.7 → 0.2.9
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/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
var un=Object.defineProperty;var j=(e,t)=>()=>(e&&(t=e(e=0)),t);var Pe=(e,t)=>{for(var n in t)un(e,n,{get:t[n],enumerable:!0})};var ot={};Pe(ot,{initProject:()=>dn});import D from"node:fs/promises";import X from"node:path";import{fileURLToPath as pn}from"node:url";async function dn(e,t){try{if((await D.readdir(e)).length>0)throw new Error(`Directory "${t}" already exists and is not empty. Choose a different name or remove the folder first.`)}catch(i){if(i.code!=="ENOENT")throw i}await D.mkdir(e,{recursive:!0}),await D.cp(mn,e,{recursive:!0});let n=X.join(e,"velu.json"),r=JSON.parse(await D.readFile(n,"utf-8"));r.name=t,await D.writeFile(n,JSON.stringify(r,null,2)+`
|
|
3
|
-
`,"utf-8");let o=await rt(e);console.log(`[velu] created ${t}/`);for(let i of o)console.log(` ${i}`);console.log(""),console.log("Next steps:"),console.log(` cd ${t}`),console.log(" velu dev")}async function rt(e,t=e,n=[]){let r=await D.readdir(e,{withFileTypes:!0});for(let o of r){let i=X.join(e,o.name);o.isDirectory()?await rt(i,t,n):n.push(X.relative(t,i).split(X.sep).join("/"))}return n.sort()}var fn,mn,it=j(()=>{fn=X.dirname(pn(import.meta.url)),mn=X.resolve(fn,"..","templates","starter")});import gn from"node:fs/promises";import Ce from"node:path";function at(e){if(!e.favicon)return null;let t=Ce.extname(e.favicon).toLowerCase();return{url:Ee+t,type:vn[t]||""}}function te(e){if(typeof e=="string"){let t=e.trim();return t?{light:t,dark:t,href:"/"}:null}if(e&&typeof e=="object"){let t=(e.light??e.dark)?.trim?.(),n=(e.dark??e.light)?.trim?.();return!t||!n?null:{light:t,dark:n,href:e.href?.trim?.()||"/"}}return null}function Fe(e){if(!e||typeof e!="object")return null;let t=[];if(e.socials&&typeof e.socials=="object")for(let[r,o]of Object.entries(e.socials))typeof o!="string"||!o.trim()||t.push({kind:bn[r]??r,href:o.trim()});let n=Array.isArray(e.links)?e.links.filter(r=>r&&typeof r=="object").map(r=>({title:(r.header??r.title??"").trim(),items:(Array.isArray(r.items)?r.items:[]).filter(o=>o&&o.label).map(o=>({label:o.label,href:o.href??"#"}))})).filter(r=>r.title||r.items.length):[];return!t.length&&!n.length?null:{socials:t,links:n}}function _e(e){return/^(https?:)?\/\//.test(e)||/^(mailto|tel):/.test(e)}function Ne(e){if(!e||typeof e!="object")return null;let t=(Array.isArray(e.links)?e.links:[]).filter(o=>o&&typeof o.href=="string"&&o.href.trim()).map(o=>({label:o.label??"",href:o.href.trim(),icon:o.icon??null,external:_e(o.href.trim())})),n=null,r=e.primary;if(r&&typeof r=="object"&&typeof r.href=="string"&&r.href.trim()){let o=r.href.trim();n=r.type==="github"?{kind:"github",label:r.label??"GitHub",href:o,external:_e(o)}:{kind:"button",label:r.label??"",href:o,external:_e(o)}}return!t.length&&!n?null:{links:t,primary:n}}function lt(e){if(e==null)return{options:[...st],display:"header"};if(typeof e!="object")return{options:[],display:"header"};let t=e.display==="toc"?"toc":"header",n=Array.isArray(e.options)?e.options:st,r=[];for(let o of n)typeof o=="string"?xn.has(o)&&r.push(o):o&&typeof o=="object"&&o.title&&o.href&&r.push({title:String(o.title),description:o.description?String(o.description):"",href:String(o.href),icon:typeof o.icon=="string"?o.icon:void 0});return{options:r,display:t}}function z(e){if(!e||typeof e!="object")return{server:null,proxy:!0,languages:void 0};let t=e.playground||{};return{server:typeof e.server=="string"?e.server.trim():null,proxy:t.proxy!==!1&&t.display!=="none",languages:Array.isArray(e.examples?.languages)?e.examples.languages:void 0}}function $n(e){return!e||typeof e!="object"?{metatags:{},indexing:"navigable"}:{metatags:e.metatags&&typeof e.metatags=="object"?e.metatags:{},indexing:e.indexing==="all"?"all":"navigable"}}function Ie(e){if(!e||typeof e!="object")return null;let t=typeof e.apiBase=="string"&&e.apiBase.trim()?e.apiBase.trim().replace(/\/+$/,""):null
|
|
3
|
+
`,"utf-8");let o=await rt(e);console.log(`[velu] created ${t}/`);for(let i of o)console.log(` ${i}`);console.log(""),console.log("Next steps:"),console.log(` cd ${t}`),console.log(" velu dev")}async function rt(e,t=e,n=[]){let r=await D.readdir(e,{withFileTypes:!0});for(let o of r){let i=X.join(e,o.name);o.isDirectory()?await rt(i,t,n):n.push(X.relative(t,i).split(X.sep).join("/"))}return n.sort()}var fn,mn,it=j(()=>{fn=X.dirname(pn(import.meta.url)),mn=X.resolve(fn,"..","templates","starter")});import gn from"node:fs/promises";import Ce from"node:path";function at(e){if(!e.favicon)return null;let t=Ce.extname(e.favicon).toLowerCase();return{url:Ee+t,type:vn[t]||""}}function te(e){if(typeof e=="string"){let t=e.trim();return t?{light:t,dark:t,href:"/"}:null}if(e&&typeof e=="object"){let t=(e.light??e.dark)?.trim?.(),n=(e.dark??e.light)?.trim?.();return!t||!n?null:{light:t,dark:n,href:e.href?.trim?.()||"/"}}return null}function Fe(e){if(!e||typeof e!="object")return null;let t=[];if(e.socials&&typeof e.socials=="object")for(let[r,o]of Object.entries(e.socials))typeof o!="string"||!o.trim()||t.push({kind:bn[r]??r,href:o.trim()});let n=Array.isArray(e.links)?e.links.filter(r=>r&&typeof r=="object").map(r=>({title:(r.header??r.title??"").trim(),items:(Array.isArray(r.items)?r.items:[]).filter(o=>o&&o.label).map(o=>({label:o.label,href:o.href??"#"}))})).filter(r=>r.title||r.items.length):[];return!t.length&&!n.length?null:{socials:t,links:n}}function _e(e){return/^(https?:)?\/\//.test(e)||/^(mailto|tel):/.test(e)}function Ne(e){if(!e||typeof e!="object")return null;let t=(Array.isArray(e.links)?e.links:[]).filter(o=>o&&typeof o.href=="string"&&o.href.trim()).map(o=>({label:o.label??"",href:o.href.trim(),icon:o.icon??null,external:_e(o.href.trim())})),n=null,r=e.primary;if(r&&typeof r=="object"&&typeof r.href=="string"&&r.href.trim()){let o=r.href.trim();n=r.type==="github"?{kind:"github",label:r.label??"GitHub",href:o,external:_e(o)}:{kind:"button",label:r.label??"",href:o,external:_e(o)}}return!t.length&&!n?null:{links:t,primary:n}}function lt(e){if(e==null)return{options:[...st],display:"header"};if(typeof e!="object")return{options:[],display:"header"};let t=e.display==="toc"?"toc":"header",n=Array.isArray(e.options)?e.options:st,r=[];for(let o of n)typeof o=="string"?xn.has(o)&&r.push(o):o&&typeof o=="object"&&o.title&&o.href&&r.push({title:String(o.title),description:o.description?String(o.description):"",href:String(o.href),icon:typeof o.icon=="string"?o.icon:void 0});return{options:r,display:t}}function z(e){if(!e||typeof e!="object")return{server:null,proxy:!0,languages:void 0};let t=e.playground||{};return{server:typeof e.server=="string"?e.server.trim():null,proxy:t.proxy!==!1&&t.display!=="none",languages:Array.isArray(e.examples?.languages)?e.examples.languages:void 0}}function $n(e){return!e||typeof e!="object"?{metatags:{},indexing:"navigable"}:{metatags:e.metatags&&typeof e.metatags=="object"?e.metatags:{},indexing:e.indexing==="all"?"all":"navigable"}}function Ie(e){if(!e||typeof e!="object")return null;let t=typeof e.apiBase=="string"&&e.apiBase.trim()?e.apiBase.trim().replace(/\/+$/,""):null,n=typeof e.host=="string"&&e.host.trim()?e.host.trim():null,r=Array.isArray(e.suggestions)?e.suggestions.filter(o=>typeof o=="string"&&o.trim()).map(o=>o.trim()).slice(0,6):[];return!t&&!r.length?null:{apiBase:t,host:n,suggestions:r}}function Re(e={},t){let n=e.colors?.primary??hn;return{name:e.name??t??"docs",url:typeof e.url=="string"&&e.url.trim()?e.url.trim().replace(/\/+$/,""):null,seo:$n(e.seo),assistant:Ie(e.assistant),thumbnails:e.thumbnails&&typeof e.thumbnails=="object"&&typeof e.thumbnails.background=="string"?{background:e.thumbnails.background}:null,colors:{primary:n,light:e.colors?.light??n,dark:e.colors?.dark??n},favicon:e.favicon??null,api:z(e.api),logo:te(e.logo),footer:Fe(e.footer),navbar:Ne(e.navbar),font:{family:e.font?.family??yn},navigation:e.navigation??null}}async function ct(e){let t=Ce.join(e,"velu.json"),n;try{n=await gn.readFile(t,"utf-8")}catch(o){throw o.code==="ENOENT"?new Error(`No velu.json found in ${e}. Run \`velu init <name>\` to scaffold one.`):o}let r;try{r=JSON.parse(n)}catch(o){throw new Error(`velu.json is not valid JSON: ${o.message}`)}return Re(r,Ce.basename(e))}function ut(e){let{colors:t,font:n}=e;return[":root:root {",` --accent-color: ${t.light};`,` --font-sans: "${n.family}", sans-serif;`,"}",':root:root[data-theme="dark"] {',` --accent-color: ${t.dark};`,"}"].join(`
|
|
4
4
|
`)}var hn,yn,fe,me,Ee,vn,bn,xn,st,Y=j(()=>{hn="#dc143c",yn="Google Sans Flex",fe="/@velu-logo-light",me="/@velu-logo-dark",Ee="/@velu-favicon",vn={".svg":"image/svg+xml",".ico":"image/x-icon",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".avif":"image/avif"};bn={twitter:"x","x-twitter":"x"};xn=new Set(["copy","assistant","view","download-pdf","download-spec","chatgpt","claude","perplexity","grok","aistudio","devin","windsurf","mcp","add-mcp","cursor","vscode","devin-mcp"]),st=["copy","view","chatgpt","claude"]});import{visit as wn}from"unist-util-visit";import{toString as Sn}from"mdast-util-to-string";import kn from"github-slugger";import{valueToEstree as jn}from"estree-util-value-to-estree";function ne(){return e=>{let t=new kn,n=[];wn(e,"heading",a=>{let s=Sn(a);n.push({depth:a.depth,id:t.slug(s),label:s})});let r={children:[]},o=[{node:r,depth:0}];for(let a of n){for(;o.length>1&&o[o.length-1].depth>=a.depth;)o.pop();let s={id:a.id,label:a.label},l=o[o.length-1].node;l.children||(l.children=[]),l.children.push(s),o.push({node:s,depth:a.depth})}let i=r.children;e.children.unshift({type:"mdxjsEsm",value:"",data:{estree:{type:"Program",sourceType:"module",body:[{type:"ExportNamedDeclaration",specifiers:[],declaration:{type:"VariableDeclaration",kind:"const",declarations:[{type:"VariableDeclarator",id:{type:"Identifier",name:"toc"},init:jn(i)}]}}]}}})}}var Ue=j(()=>{});import An from"github-slugger";import On from"iso-639-1";function Ln(e){let t=String(e).toLowerCase();return On.getNativeName(t)||e}function de(e){return new An().slug(String(e??""))}function Pn(e){let t=String(e).replace(/^\/+|\/+$/g,"").split("/").filter(Boolean);return t[t.length-1]==="index"&&t.pop(),"/"+t.join("/")}function pt(e){let t=String(e).split("#")[0].split("?")[0];try{t=decodeURI(t)}catch{}return t.startsWith("/")||(t="/"+t),t.length>1&&(t=t.replace(/\/+$/,"")),t||"/"}function U(e){let t={kind:"root",children:re(e??{})};return dt(t.children),t}function re(e){let t=Array.isArray(e.anchors)?e.anchors.map(Nn):[];if(Array.isArray(e.products))return[...t,...e.products.map(_n)];if(Array.isArray(e.versions))return[...t,...e.versions.map(Cn)];if(Array.isArray(e.languages))return[...t,...e.languages.map(En)];if(Array.isArray(e.tabs))return[...t,...e.tabs.map(Fn)];let n=[...t];return Array.isArray(e.groups)&&n.push(...e.groups.map(ft)),Array.isArray(e.pages)&&n.push(...e.pages.map(mt)),n}function _n(e){return{kind:"product",label:e.name??e.product,slug:de(e.product),icon:e.icon,color:e.color,default:!!e.default,children:re(e)}}function Cn(e){return{kind:"version",label:e.version,slug:de(e.version),default:!!e.default,children:re(e)}}function En(e){let t=String(e.language);return{kind:"language",code:t,label:Ln(t),slug:de(t),default:!!e.default,children:re(e)}}function Fn(e){return{kind:"tab",label:e.tab,slug:de(e.tab),icon:e.icon,href:e.href,children:re(e)}}function Nn(e){return{kind:"anchor",label:e.anchor,icon:e.icon,href:e.href,color:e.color}}function ft(e){let t=(e.pages??[]).map(mt);return e.root&&t.unshift({kind:"page",pagePath:e.root}),{kind:"group",label:e.group,icon:e.icon,expanded:!!e.expanded,root:e.root,children:t}}function mt(e){return typeof e=="string"?{kind:"page",pagePath:e}:e&&e.apiPage?{kind:"page",pagePath:e.apiPage,label:e.label,method:e.method,api:!0}:ft(e)}function dt(e){for(let t of Tn){let n=e.filter(r=>r.kind===t);n.length&&!n.some(r=>r.default)&&(n[0].default=!0)}for(let t of e)t.children&&dt(t.children)}function K(e){let t=[],n=(r,o,i)=>{if(r.kind==="page"){t.push({pagePath:r.pagePath,url:In(r.pagePath,o),ctx:{...o},ancestors:i,node:r});return}let a=o;r.kind==="product"?a={...o,product:r.default?null:r.slug}:r.kind==="version"?a={...o,version:r.default?null:r.slug}:r.kind==="language"&&(a={...o,language:r.default?null:r.slug});let s=r.kind==="root"?i:[...i,r];for(let l of r.children??[])n(l,a,s)};return n(e,{product:null,version:null,language:null},[]),t}function In(e,t){let n=[t.product,t.version,t.language].filter(Boolean),r=Pn(e).split("/").filter(Boolean);return"/"+[...n,...r].join("/")}var Tn,B=j(()=>{Tn=["product","version","language"]});import Rn from"node:fs";import gt from"node:path";function M(e,t){let n=[],r=[],o=new Set;for(let i of K(e)){if(i.node?.api){o.add(i.url);continue}if(o.has(i.url)){n.push(`duplicate URL "${i.url}" (page "${i.pagePath}") \u2014 keeping the first occurrence`);continue}o.add(i.url);let a=i.pagePath.replace(/^\/+|\/+$/g,""),s=gt.join(t,...a.split("/"))+".mdx",l=Rn.existsSync(s);l||n.push(`page "${i.pagePath}" \u2192 ${gt.relative(t,s)} not found`),r.push({pagePath:i.pagePath,url:i.url,fileAbsPath:s,exists:l})}return{pageEntries:r,warnings:n}}var oe=j(()=>{B()});import Un from"@apidevtools/swagger-parser";async function Bn(e){return Un.dereference(e)}function G(e){return!e||typeof e!="object"?e||{}:Array.isArray(e.allOf)?e.allOf.reduce((t,n)=>{let r=G(n);return{...t,...r,properties:{...t.properties||{},...r.properties||{}},required:[...t.required||[],...r.required||[]]}},{...e,allOf:void 0}):Array.isArray(e.oneOf)&&e.oneOf.length?G(e.oneOf[0]):Array.isArray(e.anyOf)&&e.anyOf.length?G(e.anyOf[0]):e}function ge(e){let t=G(e);if(!t||typeof t!="object")return"any";if(Array.isArray(t.enum)&&t.enum.length)return`enum<${typeof t.enum[0]}>`;if(t.type==="array")return`${ge(t.items||{})}[]`;let n=t.type||(t.properties?"object":"any");return t.format?`${n}<${t.format}>`:n}function ie(e,t=0){let n=G(e);if(!n||typeof n!="object")return null;if(n.example!==void 0)return n.example;if(n.default!==void 0)return n.default;if(Array.isArray(n.enum)&&n.enum.length)return n.enum[0];if(t>=zn)return n.type==="object"?{}:null;switch(n.type){case"object":{let r={};for(let[o,i]of Object.entries(n.properties||{}))r[o]=ie(i,t+1);return r}case"array":return[ie(n.items||{},t+1)];case"integer":case"number":return 0;case"boolean":return!0;default:return Gn(n)}}function Gn(e){switch(e.format){case"date-time":return"2024-01-01T00:00:00Z";case"date":return"2024-01-01";case"email":return"user@example.com";case"uuid":return"3fa85f64-5717-4562-b3fc-2c963f66afa6";case"uri":case"url":return"https://example.com";default:return"<string>"}}function yt(e){let t=G(e);if(!t||t.type!=="object"||!t.properties)return t&&(t.type||t.items)?[{name:"body",type:ge(t),required:!1,description:t.description||""}]:[];let n=new Set(t.required||[]);return Object.entries(t.properties).map(([r,o])=>{let i=G(o);return{name:r,type:ge(i),required:n.has(r),description:i.description||"",enum:Array.isArray(i.enum)?i.enum:void 0,deprecated:i.deprecated||void 0}})}function Jn(e){let t=e.schema||{};return{name:e.name,in:e.in,required:e.in==="path"?!0:!!e.required,type:ge(t),description:e.description||"",enum:Array.isArray(t.enum)?t.enum:void 0,example:ie(t),deprecated:e.deprecated||void 0}}function vt(e){if(!e||typeof e!="object")return null;let t=Object.keys(e),n=t.find(r=>/json/i.test(r))||t[0];return n?{contentType:n,media:e[n]}:null}function Vn(e){if(!e)return null;let t=vt(e.content);if(!t)return null;let n=t.media.schema||{};return{required:!!e.required,description:e.description||"",contentType:t.contentType,fields:yt(n),example:t.media.example??ie(n)}}function qn(e){return!e||typeof e!="object"?[]:Object.entries(e).map(([t,n])=>{let r=vt(n?.content),o=r?.media?.schema||{};return{status:t,description:n?.description||"",contentType:r?.contentType||null,fields:r?yt(o):[],example:r?r.media.example??ie(o):null}})}function Hn(e,t){if(!Array.isArray(e)||!t)return[];let n=[],r=new Set;for(let o of e)for(let i of Object.keys(o||{})){let a=t[i];!a||r.has(i)||(r.add(i),n.push(Dn(i,a)))}return n.filter(Boolean)}function Dn(e,t){return t.type==="http"&&/bearer/i.test(t.scheme||"")?{name:"Authorization",in:"header",required:!0,type:"string",prefix:"Bearer",description:t.description||"Bearer authentication token."}:t.type==="http"&&/basic/i.test(t.scheme||"")?{name:"Authorization",in:"header",required:!0,type:"string",prefix:"Basic",description:t.description||"Basic authentication credentials."}:t.type==="apiKey"?{name:t.name||e,in:t.in||"header",required:!0,type:"string",description:t.description||"API key."}:t.type==="oauth2"||t.type==="openIdConnect"?{name:"Authorization",in:"header",required:!0,type:"string",prefix:"Bearer",description:t.description||"OAuth 2.0 access token."}:{name:e,in:"header",required:!0,type:"string",description:t.description||""}}function ht(e){return String(e).replace(/\{[^}]+\}/g,t=>t.slice(1,-1)).replace(/[^a-zA-Z0-9]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase()}function Xn(e){let{method:t,path:n,op:r,sharedParams:o,servers:i,schemes:a}=e,s=[...o||[],...r.parameters||[]],l={path:[],query:[],header:[],cookie:[]};for(let u of s)u&&l[u.in]&&l[u.in].push(Jn(u));let p=r.security??e.globalSecurity;return{id:r.operationId?ht(r.operationId):`${t}-${ht(n)}`,method:t.toUpperCase(),path:n,title:r.summary||r.operationId||`${t.toUpperCase()} ${n}`,description:r.description||r.summary||"",tags:Array.isArray(r.tags)&&r.tags.length?r.tags:["API"],deprecated:r.deprecated||!1,auth:Hn(p,a),parameters:l,body:Vn(r.requestBody),responses:qn(r.responses),servers:(i||[]).map(u=>u.url).filter(Boolean)}}function Yn(e,t,n,r,o,i,a){let s={};e.openapi?s.openapi=e.openapi:e.swagger?s.swagger=e.swagger:s.openapi="3.0.0",s.info=e.info||{title:"API Reference",version:"1.0.0"};let l=(r.servers||i||[]).filter(Boolean);l.length&&(s.servers=l);let p={};Array.isArray(o)&&o.length&&(p.parameters=o),p[n]=r,s.paths={[t]:p};let u=r.security??e.security;if(a&&Array.isArray(u)&&u.length){let c={};for(let b of u)for(let d of Object.keys(b||{}))a[d]&&(c[d]=a[d]);Object.keys(c).length&&(e.swagger?s.securityDefinitions=c:s.components={securitySchemes:c})}return s}function Kn(e){let t=[],n=[],r=e.components?.securitySchemes||e.securityDefinitions||{},o=e.security||[],i=e.servers||(e.host?[{url:`https://${e.host}${e.basePath||""}`}]:[]);for(let[a,s]of Object.entries(e.paths||{})){if(!s||typeof s!="object")continue;let l=s.parameters||[],p=s.servers||i;for(let u of Mn){let c=s[u];!c||typeof c!="object"||(t.push(Xn({method:u,path:a,op:c,sharedParams:l,servers:c.servers||p,globalSecurity:o,schemes:r})),n.push(Yn(e,a,u,c,l,c.servers||p,r)))}}return{operations:t,segments:n}}async function bt(e){let t=await Bn(e),n=t.servers||(t.host?[{url:`https://${t.host}${t.basePath||""}`}]:[]),{operations:r,segments:o}=Kn(t);return{title:t.info?.title||"API Reference",version:t.info?.version||"",servers:n.map(i=>i.url).filter(Boolean),operations:r,segments:o}}var Mn,zn,xt=j(()=>{Mn=["get","post","put","patch","delete","options","head","trace"],zn=6});function Wn(e,{server:t}={}){let r=`${(t||e.servers?.[0]||"").replace(/\/+$/,"")}${e.path}`,o={};for(let a of e.auth||[])a.in==="header"&&(o[a.name]=a.prefix?`${a.prefix} <token>`:"<token>");for(let a of e.parameters?.header||[])o[a.name]||(o[a.name]=String(a.example??`<${a.type}>`));let i=!!e.body;return i&&(o["Content-Type"]=e.body.contentType||"application/json"),{method:e.method,url:r,headers:o,body:i?e.body.example??{}:null}}function Zn(e){let t=`curl --request ${e.method} \\
|
|
5
5
|
--url ${e.url}`;for(let[n,r]of Object.entries(e.headers))t+=` \\
|
|
6
6
|
--header '${n}: ${r}'`;return e.body!=null&&(t+=` \\
|
package/package.json
CHANGED
|
@@ -287,7 +287,22 @@ function ChatHeader({ onClose, onNew, onHistory, historyOpen }) {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
/* ── History overlay ────────────────────────────────────────────────── */
|
|
290
|
-
|
|
290
|
+
|
|
291
|
+
/* "Today, 10:24" / "Yesterday" / "Apr 28" for a conversation timestamp. */
|
|
292
|
+
function whenLabel(iso) {
|
|
293
|
+
if (!iso) return '';
|
|
294
|
+
const d = new Date(iso);
|
|
295
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
296
|
+
const startOfDay = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate());
|
|
297
|
+
const days = Math.round((startOfDay(new Date()) - startOfDay(d)) / 86400000);
|
|
298
|
+
if (days <= 0) {
|
|
299
|
+
return `Today, ${d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}`;
|
|
300
|
+
}
|
|
301
|
+
if (days === 1) return 'Yesterday';
|
|
302
|
+
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function HistoryPanel({ items, loading, onPick, onClose }) {
|
|
291
306
|
return (
|
|
292
307
|
<div className="velu-chatbot__history">
|
|
293
308
|
<Cluster
|
|
@@ -307,30 +322,41 @@ function HistoryPanel({ onPick, onClose }) {
|
|
|
307
322
|
</button>
|
|
308
323
|
</Cluster>
|
|
309
324
|
<Stack as="div" space="var(--s-5)" className="velu-chatbot__history-list">
|
|
310
|
-
{
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
325
|
+
{loading ? (
|
|
326
|
+
<span className="velu-chatbot__history-empty">Loading…</span>
|
|
327
|
+
) : items.length === 0 ? (
|
|
328
|
+
<span className="velu-chatbot__history-empty">No conversations yet.</span>
|
|
329
|
+
) : (
|
|
330
|
+
items.map((h) => (
|
|
331
|
+
<button
|
|
332
|
+
key={h.id}
|
|
333
|
+
type="button"
|
|
334
|
+
className="velu-chatbot__history-item"
|
|
335
|
+
onClick={() => onPick(h)}
|
|
336
|
+
>
|
|
337
|
+
<span className="velu-chatbot__history-item-title">{h.title}</span>
|
|
338
|
+
<span className="velu-chatbot__history-item-when">{h.when}</span>
|
|
339
|
+
</button>
|
|
340
|
+
))
|
|
341
|
+
)}
|
|
321
342
|
</Stack>
|
|
322
343
|
</div>
|
|
323
344
|
);
|
|
324
345
|
}
|
|
325
346
|
|
|
326
347
|
/* ── Welcome / empty state ──────────────────────────────────────────── */
|
|
327
|
-
function Welcome({ onPick }) {
|
|
348
|
+
function Welcome({ onPick, suggestions }) {
|
|
349
|
+
// Site-specific starter questions (velu.json assistant.suggestions, or
|
|
350
|
+
// publish-time generated); the canned demo list is the dev-preview fallback.
|
|
351
|
+
const items = suggestions?.length
|
|
352
|
+
? suggestions.map((label) => ({ label, icon: 'sparkles' }))
|
|
353
|
+
: SUGGESTIONS;
|
|
328
354
|
return (
|
|
329
355
|
<Stack space="var(--s0)" className="velu-chatbot__welcome">
|
|
330
356
|
<p className="velu-chatbot__welcome-hi">Ask anything about the docs.</p>
|
|
331
357
|
<span className="velu-chatbot__eyebrow">Suggested</span>
|
|
332
358
|
<Stack space="var(--s-4)">
|
|
333
|
-
{
|
|
359
|
+
{items.map((s, i) => (
|
|
334
360
|
<button
|
|
335
361
|
key={i}
|
|
336
362
|
type="button"
|
|
@@ -526,12 +552,17 @@ export default function Chatbot({
|
|
|
526
552
|
ask,
|
|
527
553
|
onFeedback,
|
|
528
554
|
onNavigate,
|
|
555
|
+
listHistory,
|
|
556
|
+
loadConversation,
|
|
557
|
+
suggestions,
|
|
529
558
|
className = '',
|
|
530
559
|
...rest
|
|
531
560
|
}) {
|
|
532
561
|
const [messages, setMessages] = useState([]);
|
|
533
562
|
const [input, setInput] = useState('');
|
|
534
563
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
564
|
+
const [history, setHistory] = useState([]);
|
|
565
|
+
const [historyLoading, setHistoryLoading] = useState(false);
|
|
535
566
|
const bodyRef = useRef(null);
|
|
536
567
|
const streamingRef = useRef(false);
|
|
537
568
|
const lastSeedRef = useRef(undefined);
|
|
@@ -567,6 +598,60 @@ export default function Chatbot({
|
|
|
567
598
|
streamingRef.current = false;
|
|
568
599
|
}, []);
|
|
569
600
|
|
|
601
|
+
// Real history: fetch the visitor's conversations when the panel opens.
|
|
602
|
+
// Without a backend (dev preview) the canned demo entries show instead.
|
|
603
|
+
useEffect(() => {
|
|
604
|
+
if (!historyOpen) return;
|
|
605
|
+
if (!listHistory) {
|
|
606
|
+
setHistory(HISTORY.map((h) => ({ ...h, demo: true })));
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
let stale = false;
|
|
610
|
+
setHistoryLoading(true);
|
|
611
|
+
listHistory()
|
|
612
|
+
.then((items) => {
|
|
613
|
+
if (stale) return;
|
|
614
|
+
setHistory(items.map((c) => ({
|
|
615
|
+
id: c.conversationId,
|
|
616
|
+
title: c.title,
|
|
617
|
+
when: whenLabel(c.updatedAt),
|
|
618
|
+
conversation: c,
|
|
619
|
+
})));
|
|
620
|
+
})
|
|
621
|
+
.catch(() => { if (!stale) setHistory([]); })
|
|
622
|
+
.finally(() => { if (!stale) setHistoryLoading(false); });
|
|
623
|
+
return () => { stale = true; };
|
|
624
|
+
}, [historyOpen, listHistory]);
|
|
625
|
+
|
|
626
|
+
// Resume a past conversation: load its turns into the panel and thread the
|
|
627
|
+
// conversation id so follow-up asks continue it.
|
|
628
|
+
const openConversation = useCallback(
|
|
629
|
+
async (item) => {
|
|
630
|
+
setHistoryOpen(false);
|
|
631
|
+
if (!item.conversation || !loadConversation) return; // demo entry
|
|
632
|
+
cancelInFlight();
|
|
633
|
+
try {
|
|
634
|
+
const { conversationId, messages: msgs } = await loadConversation(item.conversation);
|
|
635
|
+
convoRef.current = conversationId;
|
|
636
|
+
setMessages(msgs.map((m) =>
|
|
637
|
+
m.role === 'user'
|
|
638
|
+
? { role: 'user', text: m.content }
|
|
639
|
+
: {
|
|
640
|
+
role: 'ai',
|
|
641
|
+
tokens: textToTokens(m.content),
|
|
642
|
+
sources: m.citations || [],
|
|
643
|
+
followups: [],
|
|
644
|
+
streaming: false,
|
|
645
|
+
messageId: m.messageId,
|
|
646
|
+
},
|
|
647
|
+
));
|
|
648
|
+
} catch {
|
|
649
|
+
/* keep the current chat if the load fails */
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
[loadConversation, cancelInFlight],
|
|
653
|
+
);
|
|
654
|
+
|
|
570
655
|
const send = useCallback(
|
|
571
656
|
async (text) => {
|
|
572
657
|
const prompt = String(text ?? '').trim();
|
|
@@ -756,14 +841,16 @@ export default function Chatbot({
|
|
|
756
841
|
|
|
757
842
|
{historyOpen && (
|
|
758
843
|
<HistoryPanel
|
|
844
|
+
items={history}
|
|
845
|
+
loading={historyLoading}
|
|
759
846
|
onClose={() => setHistoryOpen(false)}
|
|
760
|
-
onPick={
|
|
847
|
+
onPick={openConversation}
|
|
761
848
|
/>
|
|
762
849
|
)}
|
|
763
850
|
|
|
764
851
|
<div className="velu-chatbot__body" ref={bodyRef}>
|
|
765
852
|
{messages.length === 0 ? (
|
|
766
|
-
<Welcome onPick={(t) => send(t)} />
|
|
853
|
+
<Welcome onPick={(t) => send(t)} suggestions={suggestions} />
|
|
767
854
|
) : (
|
|
768
855
|
<Stack space="var(--s1)">
|
|
769
856
|
{messages.map((m, i) =>
|
|
@@ -237,6 +237,11 @@
|
|
|
237
237
|
font-weight: var(--weight-light);
|
|
238
238
|
color: var(--muted-color);
|
|
239
239
|
}
|
|
240
|
+
.velu-chatbot__history-empty {
|
|
241
|
+
font-size: var(--f-h6);
|
|
242
|
+
color: var(--muted-color);
|
|
243
|
+
padding: var(--s-3);
|
|
244
|
+
}
|
|
240
245
|
|
|
241
246
|
/* ── Body ───────────────────────────────────────────────────────────── */
|
|
242
247
|
.velu-chatbot__body {
|
|
@@ -195,7 +195,56 @@ export function createDocsAssistant({ apiBase, host } = {}) {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
// The visitor's recent conversations on this site (the history panel).
|
|
199
|
+
// Returns [{ conversationId, token, title, updatedAt }], newest first.
|
|
200
|
+
async function listConversations() {
|
|
201
|
+
await bootstrap();
|
|
202
|
+
const res = await fetch(`${root}/conversations`, {
|
|
203
|
+
credentials: 'include',
|
|
204
|
+
headers: headers(),
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok) throw new Error(`ask-ai: conversations ${res.status}`);
|
|
207
|
+
const { conversations } = await res.json();
|
|
208
|
+
return (conversations || []).map((c) => ({
|
|
209
|
+
conversationId: c.conversation_id,
|
|
210
|
+
token: c.conversation_token,
|
|
211
|
+
title: c.title,
|
|
212
|
+
lastSeq: c.last_seq,
|
|
213
|
+
updatedAt: c.updated_at,
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Load a past conversation's messages (to resume it in the panel).
|
|
218
|
+
// Returns { conversationId, messages: [{ role, content, citations,
|
|
219
|
+
// messageId }] } with citations already renumbered to the Chatbot's Source
|
|
220
|
+
// shape. Also records the conversation's last seq so a follow-up `ask` on
|
|
221
|
+
// it streams from the right offset instead of replaying history.
|
|
222
|
+
async function loadConversation({ conversationId, token, lastSeq }) {
|
|
223
|
+
const res = await fetch(`${root}/conversations/${conversationId}`, {
|
|
224
|
+
credentials: 'include',
|
|
225
|
+
headers: headers({ Authorization: `Bearer ${token}` }),
|
|
226
|
+
});
|
|
227
|
+
if (!res.ok) throw new Error(`ask-ai: conversation ${res.status}`);
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
let maxSeq = lastSeq ?? 0;
|
|
230
|
+
const messages = (data.messages || []).map((m) => {
|
|
231
|
+
if (m.seq != null) maxSeq = Math.max(maxSeq, m.seq);
|
|
232
|
+
if (m.role !== 'assistant') {
|
|
233
|
+
return { role: m.role, content: m.content, messageId: m.id };
|
|
234
|
+
}
|
|
235
|
+
const renum = renumberCitations(m.content || '', mapCitations(m.citations));
|
|
236
|
+
return {
|
|
237
|
+
role: 'assistant',
|
|
238
|
+
content: renum.content,
|
|
239
|
+
citations: renum.citations,
|
|
240
|
+
messageId: m.id,
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
lastSeqByConv.set(conversationId, Math.max(lastSeqByConv.get(conversationId) ?? 0, maxSeq));
|
|
244
|
+
return { conversationId, messages };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { ask, sendFeedback, listConversations, loadConversation };
|
|
199
248
|
}
|
|
200
249
|
|
|
201
250
|
export default createDocsAssistant;
|
package/schema/velu.schema.json
CHANGED
|
@@ -246,8 +246,7 @@
|
|
|
246
246
|
"assistant": {
|
|
247
247
|
"type": "object",
|
|
248
248
|
"additionalProperties": false,
|
|
249
|
-
"description": "Docs-site \"Ask AI\" backend. When apiBase is set, the published build wires the chatbot to the public docs-AI-assistant API; absent → the chatbot shows a canned demo answer (also in the dev preview).",
|
|
250
|
-
"required": ["apiBase"],
|
|
249
|
+
"description": "Docs-site \"Ask AI\" backend. When apiBase is set, the published build wires the chatbot to the public docs-AI-assistant API; absent → the chatbot shows a canned demo answer (also in the dev preview). On Velu-hosted sites apiBase (and, when unset, suggestions) are filled in at publish time.",
|
|
251
250
|
"properties": {
|
|
252
251
|
"apiBase": {
|
|
253
252
|
"type": "string",
|
|
@@ -256,6 +255,12 @@
|
|
|
256
255
|
"host": {
|
|
257
256
|
"type": "string",
|
|
258
257
|
"description": "Override the tenant-resolution host sent as X-Velu-Site-Host. Defaults to the page's own host at runtime."
|
|
258
|
+
},
|
|
259
|
+
"suggestions": {
|
|
260
|
+
"type": "array",
|
|
261
|
+
"items": { "type": "string" },
|
|
262
|
+
"maxItems": 6,
|
|
263
|
+
"description": "Starter questions shown when the Ask-AI panel opens. Unset on a Velu-hosted site → generated from the site's content at publish time."
|
|
259
264
|
}
|
|
260
265
|
}
|
|
261
266
|
}
|
package/src/runtime/App.jsx
CHANGED
|
@@ -1623,6 +1623,9 @@ function DocsPage() {
|
|
|
1623
1623
|
onClose={() => setChatOpen(false)}
|
|
1624
1624
|
ask={assistant?.ask}
|
|
1625
1625
|
onFeedback={assistant?.sendFeedback}
|
|
1626
|
+
listHistory={assistant?.listConversations}
|
|
1627
|
+
loadConversation={assistant?.loadConversation}
|
|
1628
|
+
suggestions={site.assistant?.suggestions}
|
|
1626
1629
|
onNavigate={(to) => {
|
|
1627
1630
|
navigate(to);
|
|
1628
1631
|
// After the destination page mounts, scroll to the cited section
|