mirror of
https://github.com/kunkunsh/kunkun-ext-rag.git
synced 2025-04-17 12:04:31 +00:00
feat: implement UI for adding database
This commit is contained in:
parent
464edfe0df
commit
6665dccc2b
70
README.md
70
README.md
@ -1,69 +1,5 @@
|
|||||||
# Kunkun Custom UI Extension Template (SvelteKit)
|
# Kunkun RAG Extension
|
||||||
|
|
||||||
[Custom UI Extension Documentation](https://docs.kunkun.sh/extensions/custom-ui-ext/)
|
RAG means Retrieval-Augmented Generation.
|
||||||
|
|
||||||
This is a template for a custom UI extension.
|
This extension is a local RAG app, that allows you to index a local directory of files and search them using a LLM model.
|
||||||
|
|
||||||
This type of extension is basically a static website. You can use any frontend framework you like, this template uses [SvelteKit](https://svelte.dev/).
|
|
||||||
|
|
||||||
It is assumed that you have some knowledge of frontend development with SvelteKit.
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Development is the same as developing a normal website.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
pnpm dev
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
- To develop and preview the extension in Kunkun, you need to run the `Add Dev Extension` command in Kunkun, and register this extension's path.
|
|
||||||
|
|
||||||
In `package.json`, `"devMain"` is the url for development server, and `"main"` is the path to static `.html` file for production.
|
|
||||||
|
|
||||||
To load the extension in development mode, you have to enable it with `Toggle Dev Extension Live Load Mode` command in Kunkun. A `Live` badge will be shown on the commands. This indicates that dev extensions will be loaded from `devMain` instead of `main`.
|
|
||||||
|
|
||||||
## Advanced
|
|
||||||
|
|
||||||
### Rendering Mode
|
|
||||||
|
|
||||||
This is a Meta-Framework template, and already configured with SSG rendering mode.
|
|
||||||
Please do not enable SSR unless you know what you are doing.
|
|
||||||
There will not be a JS runtime in production, and Kunkun always load the extension as static files.
|
|
||||||
|
|
||||||
The main benefit of using a meta-framework is that it comes with routing, and will output multiple `.html` files, which makes multi-command support much easier.
|
|
||||||
|
|
||||||
## Verify Build and Publish
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build # make sure the build npm script works
|
|
||||||
npx kksh@latest verify # Verify some basic settings before publishing
|
|
||||||
```
|
|
||||||
|
|
||||||
It is recommended to build the extension with the same environment our CI uses.
|
|
||||||
|
|
||||||
The docker image used by our CI is `huakunshen/kunkun-ext-builder:latest`.
|
|
||||||
|
|
||||||
You can use the following command to build the extension with the same environment our CI uses.
|
|
||||||
This requires you to have docker installed, and the shell you are using has access to it via `docker` command.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx kksh@latest build # Build the extension with
|
|
||||||
```
|
|
||||||
|
|
||||||
`pnpm` is used to install dependencies and build the extension.
|
|
||||||
|
|
||||||
The docker image environment also has `node`, `pnpm`, `npm`, `bun`, `deno` installed.
|
|
||||||
If your build failed, try debug with `huakunshen/kunkun-ext-builder:latest` image in interative mode and bind your extension volume to `/workspace`.
|
|
||||||
|
|
||||||
After build successfully, you should find a tarball file ends with `.tgz` in the root of your extension.
|
|
||||||
The tarball is packaged with `npm pack` command. You can uncompress it to see if it contains all the necessary files.
|
|
||||||
|
|
||||||
This tarball is the final product that will be published and installed in Kunkun. You can further verify your extension by installing this tarball directly in Kunkun.
|
|
||||||
|
|
||||||
After verifying the tarball, it's ready to be published.
|
|
||||||
|
|
||||||
Fork [KunkunExtensions](https://github.com/kunkunsh/KunkunExtensions) repo, add your extension to the `extensions` directory, and create a PR.
|
|
||||||
|
|
||||||
Once CI passed and PR merged, you can use your extension in Kunkun.
|
|
||||||
|
180
deno-src/bucket.ts
Normal file
180
deno-src/bucket.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { FaissStore } from "@langchain/community/vectorstores/faiss";
|
||||||
|
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||||
|
import * as v from "valibot";
|
||||||
|
import * as path from "jsr:@std/path";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { Document } from "@langchain/core/documents";
|
||||||
|
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
||||||
|
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
|
||||||
|
import {
|
||||||
|
JSONLoader,
|
||||||
|
JSONLinesLoader,
|
||||||
|
} from "langchain/document_loaders/fs/json";
|
||||||
|
import { TextLoader } from "langchain/document_loaders/fs/text";
|
||||||
|
import { computeSha256FromText } from "./crypto.ts";
|
||||||
|
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
|
||||||
|
|
||||||
|
export const embeddings = new OpenAIEmbeddings({
|
||||||
|
model: "text-embedding-3-large",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MetadataSchema = v.object({
|
||||||
|
filesSha256: v.array(v.string()),
|
||||||
|
});
|
||||||
|
export type Metadata = v.InferOutput<typeof MetadataSchema>;
|
||||||
|
|
||||||
|
export async function getDocsFromDirectory(
|
||||||
|
directoryPath: string
|
||||||
|
): Promise<Document[]> {
|
||||||
|
const splitter = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: 1000,
|
||||||
|
chunkOverlap: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loader = new DirectoryLoader(directoryPath, {
|
||||||
|
".json": (path) => new JSONLoader(path, "/texts"),
|
||||||
|
".jsonl": (path) => new JSONLinesLoader(path, "/html"),
|
||||||
|
".txt": (path) => new TextLoader(path),
|
||||||
|
".md": (path) => new TextLoader(path),
|
||||||
|
".mdx": (path) => new TextLoader(path),
|
||||||
|
});
|
||||||
|
const docs = await loader.load();
|
||||||
|
const allSplits = await splitter.splitDocuments(docs);
|
||||||
|
return allSplits;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Bucket {
|
||||||
|
private readonly bucketPath: string;
|
||||||
|
private readonly faissStorePath: string;
|
||||||
|
private readonly metadataPath: string;
|
||||||
|
private _vectorStore: FaissStore | null = null;
|
||||||
|
filesSha256: Set<string> = new Set();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly bucketDir: string,
|
||||||
|
private readonly bucketName: string
|
||||||
|
) {
|
||||||
|
this.bucketPath = path.join(this.bucketDir, this.bucketName);
|
||||||
|
this.faissStorePath = path.join(this.bucketPath, "faiss-store");
|
||||||
|
this.metadataPath = path.join(this.bucketPath, "metadata.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!existsSync(this.bucketPath)) {
|
||||||
|
Deno.mkdirSync(this.bucketPath, { recursive: true });
|
||||||
|
}
|
||||||
|
if (existsSync(this.metadataPath)) {
|
||||||
|
const metadata = JSON.parse(Deno.readTextFileSync(this.metadataPath));
|
||||||
|
const parsedMetadata = v.safeParse(MetadataSchema, metadata);
|
||||||
|
if (parsedMetadata.success) {
|
||||||
|
this.filesSha256 = new Set(parsedMetadata.output.filesSha256);
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid metadata");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateMetadata();
|
||||||
|
this._vectorStore = await this.getVectorStore();
|
||||||
|
// if (this._vectorStore) {
|
||||||
|
// await this._vectorStore.save(this.faissStorePath);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMetadata() {
|
||||||
|
const metadata: Metadata = {
|
||||||
|
filesSha256: Array.from(this.filesSha256),
|
||||||
|
};
|
||||||
|
Deno.writeTextFileSync(this.metadataPath, JSON.stringify(metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVectorStore() {
|
||||||
|
if (
|
||||||
|
existsSync(this.faissStorePath) &&
|
||||||
|
existsSync(path.join(this.faissStorePath, "docstore.json"))
|
||||||
|
) {
|
||||||
|
const vectorStore = await FaissStore.load(
|
||||||
|
this.faissStorePath,
|
||||||
|
embeddings
|
||||||
|
);
|
||||||
|
return vectorStore;
|
||||||
|
}
|
||||||
|
// const vectorStore = await FaissStore.fromDocuments(docs, embeddings);
|
||||||
|
const vectorStore = new FaissStore(embeddings, {});
|
||||||
|
// await vectorStore.save(this.faissStorePath);
|
||||||
|
return vectorStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get vectorStore() {
|
||||||
|
if (this._vectorStore === null) {
|
||||||
|
throw new Error("Vector store not initialized");
|
||||||
|
}
|
||||||
|
return this._vectorStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addDocuments(documents: Document[]) {
|
||||||
|
await this.vectorStore.addDocuments(documents);
|
||||||
|
await this.vectorStore.save(this.faissStorePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillSha256(documents: Document[]) {
|
||||||
|
for (const doc of documents) {
|
||||||
|
const sha256 = computeSha256FromText(doc.pageContent);
|
||||||
|
doc.metadata.sha256 = sha256;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilteredDocs(documents: Document[]) {
|
||||||
|
return documents.filter(
|
||||||
|
(doc) => !this.filesSha256.has(doc.metadata.sha256)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSha256(docs: Document[]) {
|
||||||
|
for (const doc of docs) {
|
||||||
|
this.filesSha256.add(doc.metadata.sha256 as string);
|
||||||
|
}
|
||||||
|
this.updateMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDirectory(directoryPath: string) {
|
||||||
|
if (!existsSync(directoryPath)) {
|
||||||
|
throw new Error("Directory does not exist");
|
||||||
|
}
|
||||||
|
// check if path is a directory or file
|
||||||
|
const stats = Deno.statSync(directoryPath);
|
||||||
|
if (stats.isFile) {
|
||||||
|
throw new Error("Path is a file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await getDocsFromDirectory(directoryPath);
|
||||||
|
this.fillSha256(docs);
|
||||||
|
const fileteredDocs = this.getFilteredDocs(docs);
|
||||||
|
for (const doc of fileteredDocs) {
|
||||||
|
this.filesSha256.add(doc.metadata.sha256 as string);
|
||||||
|
}
|
||||||
|
this.updateMetadata();
|
||||||
|
const splitter = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: 1000,
|
||||||
|
chunkOverlap: 200,
|
||||||
|
});
|
||||||
|
const allSplits = await splitter.splitDocuments(fileteredDocs);
|
||||||
|
await this.addDocuments(allSplits);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTextFile(filePath: string) {
|
||||||
|
const loader = new TextLoader(filePath);
|
||||||
|
const docs = await loader.load();
|
||||||
|
this.fillSha256(docs);
|
||||||
|
const fileteredDocs = this.getFilteredDocs(docs);
|
||||||
|
this.updateSha256(docs);
|
||||||
|
await this.addDocuments(fileteredDocs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addPDF(filePath: string) {
|
||||||
|
const loader = new PDFLoader(filePath);
|
||||||
|
const docs = await loader.load();
|
||||||
|
this.fillSha256(docs);
|
||||||
|
const fileteredDocs = this.getFilteredDocs(docs);
|
||||||
|
this.updateSha256(docs);
|
||||||
|
await this.addDocuments(fileteredDocs);
|
||||||
|
}
|
||||||
|
}
|
422
deno-src/buckets/dev/faiss-store/docstore.json
Normal file
422
deno-src/buckets/dev/faiss-store/docstore.json
Normal file
File diff suppressed because one or more lines are too long
BIN
deno-src/buckets/dev/faiss-store/faiss.index
Normal file
BIN
deno-src/buckets/dev/faiss-store/faiss.index
Normal file
Binary file not shown.
1
deno-src/buckets/dev/metadata.json
Normal file
1
deno-src/buckets/dev/metadata.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"filesSha256":["bcecd6e1f4c1296ebaed37b2686a55485855bb3d04d71a16609297899ef89ba9","f67eb8d401f74305c8a4f21b2ac8986c1db3850aabe2753773ba00255fd4288c","61614a8362e20d8dc96bc8bcd52cd40b9bbf983c862253b9472f1044744bea02","f34a6fbd2e0f3fc1a1a8f3c019935e53e809d38b18fdab67ec742e02e3318a6f","e6929768ff182e0c6714a60157e94420c7942fa2cdad1f201a8302be9e86d064","b19540b04d609f5798e91d6de8ecd0fbbee9f7c3dfb008e2a198247241401c57","324d4f9a84e4849c002ae4091fe7614d2d00cb0bf4989574b307081669f9eb7a","0618796260fd0b8b4cc6daedbac59c06492a74eeebefbddf69fb13b2d2456da8","1a5d9b3ea54168ec8f89e1a93950b1d5c527c831b951070d3ffcce853c6e797f","0cc8092a90aa3f614a87258d372348706bc2f0d33e9235630849cb0aeec4342e","90ef40d14982b96bb2c4c657e17dbf74d8b15f1a36712d2759765c9e5de94995","ec62822e3eeffec14393b16a12d0dcb0920c11ad4fa1a3a6c59252ef39321c57","53f3b51bad507e0799121d30f1a96cb458d6caacda20ee6bd6447aa7a2041772","bc7551da68e709d023b76979f554252467465066285bb4c7aad07e05be82833c","a2fc83c128ebafcfc444c3740a3afcedc2e92b8c10f51724d41da57b8f670546","04c793bf24afcd608ecbbb3606ba1280602f02c678bb885b2e75f423ce394e22","3fe4c596a0bedd833fbe214081467647786a12561e3af0e4cd88a43c2787f35f","95f0df47bfd2b698f43aac7dc22ac13948fe093f838a25ab63c5f6f9b789d942","a9b8caefaa7a959928c587f74399a86549b7680894c1c882a631725a5252881e","ab68d62cb3631acf57b0704996303f2704730dd27fb30fd867092c8af90f4fb5","926688c2cc2bf5214f8ad2f82bdd68a630f0226118f50c54f8d618c2417f4c9f","4e52cee59ad6b3220e31dee7e2c9da499ce2f6c6a6a16305fefc4d05c0d63e08","c28379be7109e76bfccffcf7268b1e767c54e5f279179c773ddb33ba1a83717f","451ecba7690493bf70d26bf127bff2de09d7d5f0b51c16c363f3f372184a2988","f0b6f639135d28f12914f4fd9d8e7f11aa3c67e736cbbfbda36ec97f382b4577","a7516e047d1287385581727d5b019512288207bd2172bd241a0c6255d2b570c0","d67a47978e51dec70271dde449d9d38695a73c9787432379ab13a3b4ef78d7eb","ffbcb29742daf9184167d0e12e191e672b2d0662af402e8dfdea3f33117eb388","c19ae6410ab85759fbba1eb7c0ed49cb4f7a62759288ade5a287714e57f17823","a508c2976b25ed28733d26d2cd7601ee67aff3ad8cc0f1faf221e2ba23f325d4"]}
|
27
deno-src/crypto.ts
Normal file
27
deno-src/crypto.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
export function computeSha256(filePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
fileStream.on("data", (chunk) => {
|
||||||
|
hash.update(chunk);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on("end", () => {
|
||||||
|
resolve(hash.digest("hex"));
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on("error", (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeSha256FromText(text: string): string {
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
hash.update(text);
|
||||||
|
return hash.digest("hex");
|
||||||
|
}
|
@ -1,15 +1,19 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --watch -A --node-modules-dir --allow-scripts main.ts"
|
"dev": "deno run --watch -A --node-modules-dir --allow-scripts main.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@langchain/community": "npm:@langchain/community@^0.3.22",
|
"@kunkun/api": "jsr:@kunkun/api@^0.0.52",
|
||||||
"@langchain/core": "npm:@langchain/core@^0.3.27",
|
"@langchain/community": "npm:@langchain/community@^0.3.22",
|
||||||
"@langchain/langgraph": "npm:@langchain/langgraph@^0.2.38",
|
"@langchain/core": "npm:@langchain/core@^0.3.27",
|
||||||
"@langchain/openai": "npm:@langchain/openai@^0.3.16",
|
"@langchain/langgraph": "npm:@langchain/langgraph@^0.2.38",
|
||||||
"@langchain/textsplitters": "npm:@langchain/textsplitters@^0.1.0",
|
"@langchain/openai": "npm:@langchain/openai@^0.3.16",
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@langchain/textsplitters": "npm:@langchain/textsplitters@^0.1.0",
|
||||||
"faiss-node": "npm:faiss-node@^0.5.1",
|
"pdf-parse": "npm:pdf-parse@^1.1.1",
|
||||||
"langchain": "npm:langchain@^0.3.9"
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
}
|
"valibot": "jsr:@valibot/valibot@^0.42.1",
|
||||||
|
"faiss-node": "npm:faiss-node@^0.5.1",
|
||||||
|
"langchain": "npm:langchain@^0.3.12"
|
||||||
|
},
|
||||||
|
"nodeModulesDir": "auto"
|
||||||
}
|
}
|
||||||
|
1373
deno-src/deno.lock
generated
1373
deno-src/deno.lock
generated
File diff suppressed because it is too large
Load Diff
20
deno-src/dev.ts
Normal file
20
deno-src/dev.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { FaissStore } from "@langchain/community/vectorstores/faiss";
|
||||||
|
import { Bucket, embeddings, getDocsFromDirectory } from "./bucket.ts";
|
||||||
|
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
|
||||||
|
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
|
||||||
|
import {
|
||||||
|
JSONLoader,
|
||||||
|
JSONLinesLoader,
|
||||||
|
} from "langchain/document_loaders/fs/json";
|
||||||
|
import { TextLoader } from "langchain/document_loaders/fs/text";
|
||||||
|
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||||
|
|
||||||
|
const bucket = new Bucket(
|
||||||
|
"/Users/hk/Dev/kunkun-extension-repos/kunkun-ext-rag/deno-src/buckets",
|
||||||
|
"dev"
|
||||||
|
);
|
||||||
|
await bucket.init();
|
||||||
|
// await bucket.addDirectory(
|
||||||
|
// "/Users/hk/Dev/kunkun-docs/src/content/docs/guides/Extensions/Publish"
|
||||||
|
// );
|
||||||
|
// await bucket.addPDF("/Users/hk/Downloads/WACV_2025_Caroline_Huakun__Copy_.pdf");
|
6
deno-src/index.ts
Normal file
6
deno-src/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { expose } from "@kunkun/api/runtime/deno";
|
||||||
|
|
||||||
|
expose({
|
||||||
|
|
||||||
|
})
|
||||||
|
|
117
deno-src/main.ts
117
deno-src/main.ts
@ -14,7 +14,7 @@ import {
|
|||||||
} from "langchain/document_loaders/fs/json";
|
} from "langchain/document_loaders/fs/json";
|
||||||
import { TextLoader } from "langchain/document_loaders/fs/text";
|
import { TextLoader } from "langchain/document_loaders/fs/text";
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
export const embeddings = new OpenAIEmbeddings({
|
||||||
model: "text-embedding-3-large",
|
model: "text-embedding-3-large",
|
||||||
});
|
});
|
||||||
async function getVectorStore(): Promise<FaissStore> {
|
async function getVectorStore(): Promise<FaissStore> {
|
||||||
@ -23,14 +23,19 @@ async function getVectorStore(): Promise<FaissStore> {
|
|||||||
return FaissStore.load("./FaissStore", embeddings);
|
return FaissStore.load("./FaissStore", embeddings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loader = new DirectoryLoader("/Users/hk/Desktop/rag-ts/datasets", {
|
const loader = new DirectoryLoader(
|
||||||
".json": (path) => new JSONLoader(path, "/texts"),
|
"/Users/hk/Dev/kunkun-extension-repos/kunkun-ext-rag/deno-src/buckets",
|
||||||
".jsonl": (path) => new JSONLinesLoader(path, "/html"),
|
// "/Users/hk/Dev/kunkun-docs/src/content/docs",
|
||||||
".txt": (path) => new TextLoader(path),
|
{
|
||||||
".md": (path) => new TextLoader(path),
|
".json": (path) => new JSONLoader(path, "/texts"),
|
||||||
});
|
".jsonl": (path) => new JSONLinesLoader(path, "/html"),
|
||||||
|
".txt": (path) => new TextLoader(path),
|
||||||
|
".md": (path) => new TextLoader(path),
|
||||||
|
".mdx": (path) => new TextLoader(path),
|
||||||
|
}
|
||||||
|
);
|
||||||
const docs = await loader.load();
|
const docs = await loader.load();
|
||||||
console.log(docs);
|
// console.log(docs);
|
||||||
|
|
||||||
const splitter = new RecursiveCharacterTextSplitter({
|
const splitter = new RecursiveCharacterTextSplitter({
|
||||||
chunkSize: 1000,
|
chunkSize: 1000,
|
||||||
@ -55,60 +60,60 @@ async function deleteDocuments(vectorStore: FaissStore, ids: string[]) {
|
|||||||
|
|
||||||
const vectorStore = await getVectorStore();
|
const vectorStore = await getVectorStore();
|
||||||
|
|
||||||
const llm = new ChatOpenAI({
|
// const llm = new ChatOpenAI({
|
||||||
model: "gpt-4o-mini",
|
// model: "gpt-4o-mini",
|
||||||
temperature: 0,
|
// temperature: 0,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Define prompt for question-answering
|
// // Define prompt for question-answering
|
||||||
const promptTemplate = await pull<ChatPromptTemplate>("rlm/rag-prompt");
|
// const promptTemplate = await pull<ChatPromptTemplate>("rlm/rag-prompt");
|
||||||
|
|
||||||
// Define state for application
|
// // Define state for application
|
||||||
const InputStateAnnotation = Annotation.Root({
|
// const InputStateAnnotation = Annotation.Root({
|
||||||
question: Annotation<string>,
|
// question: Annotation<string>,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const StateAnnotation = Annotation.Root({
|
// const StateAnnotation = Annotation.Root({
|
||||||
question: Annotation<string>,
|
// question: Annotation<string>,
|
||||||
context: Annotation<Document[]>,
|
// context: Annotation<Document[]>,
|
||||||
answer: Annotation<string>,
|
// answer: Annotation<string>,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Define application steps
|
// // Define application steps
|
||||||
const retrieve = async (state: typeof InputStateAnnotation.State) => {
|
// const retrieve = async (state: typeof InputStateAnnotation.State) => {
|
||||||
const retrievedDocs = await vectorStore.similaritySearch(state.question);
|
// const retrievedDocs = await vectorStore.similaritySearch(state.question);
|
||||||
return { context: retrievedDocs };
|
// return { context: retrievedDocs };
|
||||||
};
|
// };
|
||||||
|
|
||||||
const generate = async (state: typeof StateAnnotation.State) => {
|
// const generate = async (state: typeof StateAnnotation.State) => {
|
||||||
const docsContent = state.context.map((doc) => doc.pageContent).join("\n");
|
// const docsContent = state.context.map((doc) => doc.pageContent).join("\n");
|
||||||
const messages = await promptTemplate.invoke({
|
// const messages = await promptTemplate.invoke({
|
||||||
question: state.question,
|
// question: state.question,
|
||||||
context: docsContent,
|
// context: docsContent,
|
||||||
});
|
// });
|
||||||
const response = await llm.invoke(messages);
|
// const response = await llm.invoke(messages);
|
||||||
return { answer: response.content };
|
// return { answer: response.content };
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Compile application and test
|
// // Compile application and test
|
||||||
const graph = new StateGraph(StateAnnotation)
|
// const graph = new StateGraph(StateAnnotation)
|
||||||
.addNode("retrieve", retrieve)
|
// .addNode("retrieve", retrieve)
|
||||||
.addNode("generate", generate)
|
// .addNode("generate", generate)
|
||||||
.addEdge("__start__", "retrieve")
|
// .addEdge("__start__", "retrieve")
|
||||||
.addEdge("retrieve", "generate")
|
// .addEdge("retrieve", "generate")
|
||||||
.addEdge("generate", "__end__")
|
// .addEdge("generate", "__end__")
|
||||||
.compile();
|
// .compile();
|
||||||
|
|
||||||
let inputs = { question: "What is Task Decomposition?" };
|
// let inputs = { question: "What is Task Decomposition?" };
|
||||||
|
|
||||||
while (true) {
|
// while (true) {
|
||||||
const question = prompt("Enter your question (or 'exit' to quit): ");
|
// const question = prompt("Enter your question (or 'exit' to quit): ");
|
||||||
if (!question || question.toLowerCase() === "exit") {
|
// if (!question || question.toLowerCase() === "exit") {
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const result = await graph.invoke({ question });
|
// const result = await graph.invoke({ question });
|
||||||
console.log("\nAnswer:");
|
// console.log("\nAnswer:");
|
||||||
console.log(result.answer);
|
// console.log(result.answer);
|
||||||
console.log("\n-------------------\n");
|
// console.log("\n-------------------\n");
|
||||||
}
|
// }
|
||||||
|
26
package.json
26
package.json
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.kunkun.sh",
|
"$schema": "https://schema.kunkun.sh",
|
||||||
|
"license": "MIT",
|
||||||
"name": "kunkun-ext-rag",
|
"name": "kunkun-ext-rag",
|
||||||
"draft": true,
|
"draft": true,
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
@ -11,7 +12,7 @@
|
|||||||
"identifier": "RAG",
|
"identifier": "RAG",
|
||||||
"icon": {
|
"icon": {
|
||||||
"type": "iconify",
|
"type": "iconify",
|
||||||
"value": "logos:svelte-icon"
|
"value": "carbon:rag"
|
||||||
},
|
},
|
||||||
"demoImages": [],
|
"demoImages": [],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@ -23,14 +24,7 @@
|
|||||||
"main": "/",
|
"main": "/",
|
||||||
"dist": "build",
|
"dist": "build",
|
||||||
"devMain": "http://localhost:5173",
|
"devMain": "http://localhost:5173",
|
||||||
"name": "Sveltekit Template Home Page",
|
"name": "RAG",
|
||||||
"cmds": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"main": "about",
|
|
||||||
"dist": "build",
|
|
||||||
"devMain": "http://localhost:5173/about",
|
|
||||||
"name": "Sveltekit Template About Page",
|
|
||||||
"cmds": []
|
"cmds": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -46,13 +40,15 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kksh/api": "^0.0.48",
|
"@iconify/svelte": "^4.2.0",
|
||||||
"@kksh/svelte5": "0.1.10",
|
"@kksh/api": "^0.0.54",
|
||||||
|
"@kksh/svelte5": "0.1.15",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-svelte": "^0.460.1",
|
"lucide-svelte": "^0.474.0",
|
||||||
"mode-watcher": "^0.5.0",
|
"mode-watcher": "^0.5.1",
|
||||||
"tailwind-merge": "^2.5.4",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwind-variants": "^0.3.0"
|
"tailwind-variants": "^0.3.1",
|
||||||
|
"valibot": "^1.0.0-beta.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.3.1",
|
"@sveltejs/adapter-auto": "^3.3.1",
|
||||||
|
1
src/lib/ai.ts
Normal file
1
src/lib/ai.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// export const constructAiAPIKey = (database: string) => `rag-api-key-${database}`;
|
35
src/lib/components/DatabaseList.svelte
Normal file
35
src/lib/components/DatabaseList.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { DBInfo } from '@/models';
|
||||||
|
import { dbStore } from '@/stores/db';
|
||||||
|
import { toast } from '@kksh/api/headless';
|
||||||
|
import { Button, Card } from '@kksh/svelte5';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet dbInfoCard(dbInfo: DBInfo)}
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{dbInfo.name}</Card.Title>
|
||||||
|
<Card.Description><strong>AI Provider:</strong> {dbInfo.ai}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="lg"
|
||||||
|
onclick={() =>
|
||||||
|
dbStore
|
||||||
|
.deleteById(dbInfo.id)
|
||||||
|
.then(() => dbStore.load())
|
||||||
|
.then(() => toast.success('Database deleted'))
|
||||||
|
.catch((err) => toast.error('Fail to delete database', { description: err.message }))}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each $dbStore as db}
|
||||||
|
{@render dbInfoCard(db)}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
49
src/lib/components/app-sidebar.svelte
Normal file
49
src/lib/components/app-sidebar.svelte
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ui } from '@kksh/api/ui/iframe';
|
||||||
|
import { Sidebar } from '@kksh/svelte5';
|
||||||
|
import Icon from '@iconify/svelte';
|
||||||
|
import { BotIcon, DatabaseIcon, MessageCircleIcon } from 'lucide-svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
ui.showBackButton('bottom-right');
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: 'Database',
|
||||||
|
url: '/',
|
||||||
|
icon: DatabaseIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Chat',
|
||||||
|
url: '/chat',
|
||||||
|
icon: MessageCircleIcon
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Root>
|
||||||
|
<Sidebar.Content>
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupLabel data-kunkun-drag-region>Menu</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each items as item (item.title)}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={item.url} {...props}>
|
||||||
|
<!-- <Icon icon={item.icon} /> -->
|
||||||
|
<item.icon />
|
||||||
|
<span>{item.title}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
</Sidebar.Content>
|
||||||
|
</Sidebar.Root>
|
87
src/lib/components/forms/AddBucket.svelte
Normal file
87
src/lib/components/forms/AddBucket.svelte
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { DBInfo } from '@/models';
|
||||||
|
import { Form, Input, Select } from '@kksh/svelte5';
|
||||||
|
import SuperDebug, { defaults, superForm } from 'sveltekit-superforms';
|
||||||
|
import { valibot, valibotClient } from 'sveltekit-superforms/adapters';
|
||||||
|
import * as v from 'valibot';
|
||||||
|
|
||||||
|
let {
|
||||||
|
class: className,
|
||||||
|
onSubmit
|
||||||
|
}: {
|
||||||
|
class: string;
|
||||||
|
onSubmit: (data: Omit<DBInfo, 'id'>) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const aiChoices = [{ value: 'openai', label: 'OpenAI' }];
|
||||||
|
|
||||||
|
const npmPackageNameFormSchema = v.object({
|
||||||
|
name: v.pipe(v.string(), v.minLength(1)),
|
||||||
|
apiKey: v.pipe(v.string(), v.minLength(1)),
|
||||||
|
ai: v.pipe(v.string(), v.minLength(1))
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = superForm(defaults(valibot(npmPackageNameFormSchema)), {
|
||||||
|
validators: valibotClient(npmPackageNameFormSchema),
|
||||||
|
SPA: true,
|
||||||
|
onUpdate({ form, cancel }) {
|
||||||
|
if (!form.valid) {
|
||||||
|
console.log('invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSubmit(form.data);
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { form: formData, enhance, errors } = form;
|
||||||
|
$formData.ai = 'openai'; // set default value
|
||||||
|
const triggerContent = $derived(
|
||||||
|
aiChoices.find((f) => f.value === $formData.ai)?.label ?? 'Select a AI'
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" use:enhance class={className}>
|
||||||
|
<Form.Field {form} name="name">
|
||||||
|
<Form.Control>
|
||||||
|
{#snippet children({ props })}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input {...props} bind:value={$formData.name} placeholder="Database Name" />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Form.Field {form} name="ai">
|
||||||
|
<Form.Control>
|
||||||
|
{#snippet children({ props })}
|
||||||
|
<Select.Root type="single" name="favoriteFruit" bind:value={$formData.ai}>
|
||||||
|
<Select.Trigger class="w-[180px]">
|
||||||
|
{triggerContent}
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
<Select.Group>
|
||||||
|
<Select.GroupHeading>AI Provider</Select.GroupHeading>
|
||||||
|
{#each aiChoices as ai}
|
||||||
|
<Select.Item value={ai.value} label={ai.label}>{ai.label}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Group>
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
{/snippet}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field {form} name="apiKey" class="grow">
|
||||||
|
<Form.Control>
|
||||||
|
{#snippet children({ props })}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input {...props} bind:value={$formData.apiKey} placeholder="OpenAI API Key" />
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.FieldErrors />
|
||||||
|
</Form.Field>
|
||||||
|
</div>
|
||||||
|
<Form.Button class="w-full">Add</Form.Button>
|
||||||
|
</form>
|
1
src/lib/constants.ts
Normal file
1
src/lib/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const DB_DATATYPE_DATABASE = 'database';
|
9
src/lib/models.ts
Normal file
9
src/lib/models.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import * as v from 'valibot';
|
||||||
|
|
||||||
|
export const DBInfo = v.object({
|
||||||
|
id: v.number(),
|
||||||
|
name: v.string(),
|
||||||
|
ai: v.string(),
|
||||||
|
apiKey: v.string()
|
||||||
|
});
|
||||||
|
export type DBInfo = v.InferOutput<typeof DBInfo>;
|
56
src/lib/stores/db.ts
Normal file
56
src/lib/stores/db.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { DBInfo } from '@/models';
|
||||||
|
import { get, writable } from 'svelte/store';
|
||||||
|
import { db as dbAPI } from '@kksh/api/ui/iframe';
|
||||||
|
import { DB_DATATYPE_DATABASE } from '@/constants';
|
||||||
|
import * as v from 'valibot';
|
||||||
|
import { SearchModeEnum } from '@kksh/api/models';
|
||||||
|
|
||||||
|
export const createDbStore = () => {
|
||||||
|
const databases = writable<DBInfo[]>([]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...databases,
|
||||||
|
load() {
|
||||||
|
return dbAPI
|
||||||
|
.search({ dataType: DB_DATATYPE_DATABASE, fields: ['data', 'search_text'] })
|
||||||
|
.then((records) => {
|
||||||
|
const parsedBuckets = records
|
||||||
|
.map((rec) => {
|
||||||
|
if (!rec.data) return null;
|
||||||
|
const parse = v.safeParse(DBInfo, { id: rec.dataId, ...JSON.parse(rec.data) });
|
||||||
|
if (parse.success) {
|
||||||
|
return parse.output;
|
||||||
|
}
|
||||||
|
console.error('Invalid database info', rec);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((rec) => rec !== null);
|
||||||
|
|
||||||
|
console.log('parsedBuckets', parsedBuckets);
|
||||||
|
databases.set(parsedBuckets);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return get(databases);
|
||||||
|
},
|
||||||
|
add(dbInfo: Omit<DBInfo, 'id'>) {
|
||||||
|
return dbAPI.add({
|
||||||
|
data: JSON.stringify(dbInfo),
|
||||||
|
dataType: DB_DATATYPE_DATABASE,
|
||||||
|
searchText: dbInfo.name
|
||||||
|
});
|
||||||
|
},
|
||||||
|
dbExists(dbName: string) {
|
||||||
|
return dbAPI.search({
|
||||||
|
dataType: DB_DATATYPE_DATABASE,
|
||||||
|
searchText: dbName,
|
||||||
|
searchMode: SearchModeEnum.ExactMatch
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteById(dataId: number) {
|
||||||
|
return dbAPI.delete(dataId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dbStore = createDbStore();
|
@ -4,16 +4,29 @@
|
|||||||
import { ThemeWrapper, updateTheme } from '@kksh/svelte5';
|
import { ThemeWrapper, updateTheme } from '@kksh/svelte5';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { ui } from '@kksh/api/ui/iframe';
|
import { ui } from '@kksh/api/ui/iframe';
|
||||||
|
import { Sidebar } from '@kksh/svelte5';
|
||||||
|
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||||
|
import { DB_DATATYPE_DATABASE } from '@/constants';
|
||||||
|
import { dbStore } from '@/stores/db';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ui.registerDragRegion();
|
ui.registerDragRegion();
|
||||||
ui.getTheme().then((theme) => {
|
ui.getTheme().then((theme) => {
|
||||||
updateTheme(theme);
|
updateTheme(theme);
|
||||||
});
|
});
|
||||||
|
dbStore.load();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<slot />
|
<Sidebar.Provider>
|
||||||
|
<AppSidebar />
|
||||||
|
<main class="w-full">
|
||||||
|
<Sidebar.Trigger />
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</Sidebar.Provider>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
|
@ -1,101 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { base } from '$app/paths';
|
import { db, ui, kv, toast } from '@kksh/api/ui/iframe';
|
||||||
import { clipboard, notification, ui, toast } from '@kksh/api/ui/iframe';
|
import AddBucket from '$lib/components/forms/AddBucket.svelte';
|
||||||
import {
|
import type { DBInfo } from '@/models';
|
||||||
ModeToggle,
|
|
||||||
Button,
|
|
||||||
Command,
|
|
||||||
ModeWatcher,
|
|
||||||
Separator,
|
|
||||||
ThemeWrapper,
|
|
||||||
updateTheme
|
|
||||||
} from '@kksh/svelte5';
|
|
||||||
import ThemeCustomizer from '$lib/components/ThemeCustomizer.svelte';
|
|
||||||
import {
|
|
||||||
Calculator,
|
|
||||||
Calendar,
|
|
||||||
CreditCard,
|
|
||||||
Settings,
|
|
||||||
SettingsIcon,
|
|
||||||
Smile,
|
|
||||||
User
|
|
||||||
} from 'lucide-svelte';
|
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { DB_DATATYPE_DATABASE } from '@/constants';
|
||||||
|
import { dbStore } from '@/stores/db';
|
||||||
|
import DatabaseList from '@/components/DatabaseList.svelte';
|
||||||
|
|
||||||
onMount(() => {
|
async function onSubmit(data: Omit<DBInfo, 'id'>) {
|
||||||
ui.registerDragRegion();
|
dbStore
|
||||||
notification.sendNotification('Hello from template-ext-svelte');
|
.add(data)
|
||||||
ui.getTheme().then((theme) => {
|
.then(() => {
|
||||||
updateTheme(theme);
|
toast.success('Database added');
|
||||||
});
|
return dbStore.load();
|
||||||
});
|
})
|
||||||
|
.catch((err) => {
|
||||||
let highlighted = '';
|
toast.error('Fail to add database to extension storage', { description: err.message });
|
||||||
let searchTerm = '';
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModeWatcher />
|
<div class="container mx-auto">
|
||||||
|
<AddBucket class="w-full" {onSubmit} />
|
||||||
<ThemeWrapper>
|
<br />
|
||||||
<Command.Root class="h-screen rounded-lg border shadow-md" bind:value={highlighted}>
|
<h1 class="text-2xl font-bold">Databases</h1>
|
||||||
<Command.Input placeholder="Type a command or search..." autofocus bind:value={searchTerm} />
|
<DatabaseList />
|
||||||
<div class="grow">
|
</div>
|
||||||
<Command.List>
|
|
||||||
<Command.Empty>No results found.</Command.Empty>
|
|
||||||
<Command.Group heading="Suggestions">
|
|
||||||
<Command.Item>
|
|
||||||
<Calendar class="mr-2 h-4 w-4" />
|
|
||||||
|
|
||||||
<span>Calendar</span>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item>
|
|
||||||
<Smile class="mr-2 h-4 w-4" />
|
|
||||||
<span>Search Emoji</span>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item>
|
|
||||||
<Calculator class="mr-2 h-4 w-4" />
|
|
||||||
<span>Calculator</span>
|
|
||||||
</Command.Item>
|
|
||||||
</Command.Group>
|
|
||||||
<Command.Separator />
|
|
||||||
<Command.Group heading="Settings">
|
|
||||||
<Command.Item>
|
|
||||||
<User class="mr-2 h-4 w-4" />
|
|
||||||
<span>Profile</span>
|
|
||||||
<Command.Shortcut>⌘P</Command.Shortcut>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item value="billllling">
|
|
||||||
<CreditCard class="mr-2 h-4 w-4" />
|
|
||||||
<span>Billing</span>
|
|
||||||
<Command.Shortcut>⌘B</Command.Shortcut>
|
|
||||||
</Command.Item>
|
|
||||||
<Command.Item>
|
|
||||||
<Settings class="mr-2 h-4 w-4" />
|
|
||||||
<span>Settings</span>
|
|
||||||
<Command.Shortcut>⌘S</Command.Shortcut>
|
|
||||||
</Command.Item>
|
|
||||||
</Command.Group>
|
|
||||||
</Command.List>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<SettingsIcon class="ml-2 h-4 w-4" />
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Button variant="ghost" size="sm">
|
|
||||||
Open Application
|
|
||||||
<kbd class="ml-1">↵</kbd>
|
|
||||||
</Button>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<a href="{base}/about"><Button>About Page</Button></a>
|
|
||||||
<Button
|
|
||||||
onclick={async () => {
|
|
||||||
toast.success(await clipboard.readText());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Read Clipboard
|
|
||||||
</Button>
|
|
||||||
<ModeToggle />
|
|
||||||
<ThemeCustomizer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Command.Root>
|
|
||||||
</ThemeWrapper>
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user