Skip to content

Commit df8ff16

Browse files
committed
feat: store server api key on client side
1 parent 66ce58e commit df8ff16

14 files changed

Lines changed: 1330 additions & 207 deletions

File tree

package-lock.json

Lines changed: 1002 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"dependencies": {
3+
"@google/generative-ai": "^0.24.1",
4+
"@huggingface/transformers": "^3.6.3",
35
"dom-accessibility-api": "^0.7.0",
46
"marked": "^15.0.7",
57
"notivue": "^2.4.5",

src/background/service.ts

Lines changed: 95 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import {
77
Settings,
88
SelectInputContext,
99
} from "@/types/common"
10-
import { getMessageForStatusCode } from "@/utils/auth"
10+
import {
11+
buildMultiSelectInputDataExtractionPrompt,
12+
buildSingleSelectInputDataExtractionPrompt,
13+
buildTextInputDataExtractionPrompt,
14+
wrapContext,
15+
} from "./services/prompts"
16+
import { AccurateExtractResult } from "./services/types"
17+
import Llmservice from "./services/llm"
1118

1219
const API_URL = import.meta.env.VITE_API_URL
1320
const FILL_ACCURATE = `${API_URL}/fill/accurate`
@@ -18,12 +25,7 @@ export async function getAccurateFillData(
1825
) {
1926
const settings = await getValueFromStorage<Settings>("settings", "sync")
2027
const activeProfileId = settings?.activeProfileId ?? "default"
21-
22-
const tokens = await getValueFromStorage<{ server: string }>(
23-
"tokens",
24-
"local",
25-
)
26-
28+
const llmApiKey = settings.llmApiKey
2729
const userContext = await getValueFromStorage<AccurateDetails>(
2830
`${activeProfileId}-${DETAIL_TYPES.SHORT}`,
2931
"sync",
@@ -49,8 +51,12 @@ export async function getAccurateFillData(
4951
userContextCreativeFill,
5052
)
5153

52-
//console.info("User context", userTotalContext)
53-
//console.info("Input context", textInputContext)
54+
if (!llmApiKey) {
55+
return {
56+
success: false,
57+
message: "Please add your Gemini API key from settings",
58+
}
59+
}
5460

5561
if (!userTotalContext.length) {
5662
return {
@@ -59,45 +65,90 @@ export async function getAccurateFillData(
5965
}
6066
}
6167

62-
try {
63-
const response = await fetch(FILL_ACCURATE, {
64-
method: "POST",
65-
headers: {
66-
"Content-Type": "application/json",
67-
Authorization: `Bearer ${tokens?.server}`,
68-
},
69-
body: JSON.stringify({
70-
context: userTotalContext,
71-
text_input_context: textInputContext,
72-
select_context: selectContext,
73-
}),
74-
})
7568

76-
const statusCode = response.status
77-
if (!response.ok) {
78-
if (statusCode === 401) {
79-
chrome.storage.local.remove("tokens")
80-
}
69+
const llmService = new Llmservice(llmApiKey)
70+
const extractedData = await passContextAndGetAnswers(
71+
llmService,
72+
userTotalContext,
73+
textInputContext,
74+
selectContext,
75+
)
8176

82-
return {
83-
success: false,
84-
message: getMessageForStatusCode(statusCode),
85-
error: "Network response was not ok",
86-
}
87-
}
77+
return {
78+
success: true,
79+
data: extractedData,
80+
}
81+
}
8882

89-
const data = await response.json()
83+
// LLMs cooking
84+
async function passContextAndGetAnswers(
85+
llmService: Llmservice,
86+
userContext: { label: string; value: string }[],
87+
textInputContext: TextInputContext[],
88+
selectContext: SelectInputContext[],
89+
) {
90+
const contextPrompt = wrapContext(userContext)
91+
let result: AccurateExtractResult[] = []
9092

91-
return {
92-
success: true,
93-
data,
94-
}
95-
} catch (error) {
96-
console.info("fetch error", error)
97-
return {
98-
success: false,
99-
message: getMessageForStatusCode(503),
100-
error,
93+
if (textInputContext.length) {
94+
const textPrompt = buildTextInputDataExtractionPrompt(
95+
contextPrompt,
96+
textInputContext,
97+
)
98+
const schema: object = [
99+
{
100+
dataId: "abc-uuid",
101+
value: "answer or null",
102+
},
103+
]
104+
const promptResult =
105+
(await llmService.getResultWithSchema(textPrompt, schema)) ?? []
106+
result = result.concat(promptResult as AccurateExtractResult[])
107+
}
108+
109+
const singleSelectContext = []
110+
const multiSelectContext = []
111+
const singleSelectTags = ["option", "radio"]
112+
for (const ctx of selectContext) {
113+
if (singleSelectTags.includes(ctx.tagName ?? "")) {
114+
singleSelectContext.push(ctx)
115+
} else {
116+
multiSelectContext.push(ctx)
101117
}
102118
}
119+
120+
if (singleSelectContext.length) {
121+
const selectPrompt = buildSingleSelectInputDataExtractionPrompt(
122+
contextPrompt,
123+
singleSelectContext,
124+
)
125+
const schema: object = [
126+
{
127+
dataId: "abc-uuid",
128+
value: "correct option",
129+
},
130+
]
131+
const promptResult =
132+
(await llmService.getResultWithSchema(selectPrompt, schema)) ?? []
133+
result = result.concat(promptResult as AccurateExtractResult[])
134+
}
135+
136+
if (multiSelectContext.length) {
137+
const selectPrompt = buildMultiSelectInputDataExtractionPrompt(
138+
contextPrompt,
139+
multiSelectContext,
140+
)
141+
const schema: object = [
142+
{
143+
dataId: "abc-uuid",
144+
value: "correct option 1 | correct option 2",
145+
},
146+
]
147+
148+
const promptResult =
149+
(await llmService.getResultWithSchema(selectPrompt, schema)) ?? []
150+
result = result.concat(promptResult as AccurateExtractResult[])
151+
}
152+
153+
return result.filter((v) => v)
103154
}

src/background/services/llm.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { GenerativeModel, GoogleGenerativeAI } from "@google/generative-ai"
2+
3+
export default class Llmservice {
4+
private genAI: GoogleGenerativeAI
5+
private textModel: GenerativeModel
6+
private jsonModel: GenerativeModel
7+
8+
constructor(apiKey: string) {
9+
this.genAI = new GoogleGenerativeAI(apiKey)
10+
// For text-only input, use the gemini-pro model
11+
this.textModel = this.genAI.getGenerativeModel({
12+
model: "gemini-2.5-flash",
13+
})
14+
// For JSON output, we'll use the same model and guide it with the prompt
15+
this.jsonModel = this.genAI.getGenerativeModel({
16+
model: "gemini-2.5-flash",
17+
})
18+
}
19+
20+
/**
21+
* Generates a structured JSON object based on the message and schema.
22+
* @param message The prompt message for the model.
23+
* @param schema The desired JSON schema (provided as a string hint).
24+
* @returns A promise that resolves to a JavaScript object.
25+
*/
26+
async getResultWithSchema(
27+
message: string,
28+
schema: object,
29+
): Promise<object | null> {
30+
const schemaHint = JSON.stringify(schema)
31+
const fullPrompt = `${message}. Please provide the output in the following JSON format: ${schemaHint}. Do not include any additional text or explanations, only the JSON object.`
32+
33+
try {
34+
const result = await this.jsonModel.generateContent(fullPrompt)
35+
const response = await result.response
36+
let text = response.text()
37+
// Attempt to extract pure JSON by stripping markdown code block
38+
text = text.trim()
39+
if (text.startsWith("```json") && text.endsWith("```")) {
40+
text = text.substring("```json".length).trim()
41+
text = text.substring(0, text.length - "```".length).trim()
42+
}
43+
return JSON.parse(text)
44+
} catch (error) {
45+
console.error(
46+
"Failed to generate or parse JSON from model output:",
47+
error,
48+
)
49+
return null
50+
}
51+
}
52+
53+
/**
54+
* Generates a plain text result from a given message.
55+
* @param message The prompt message for the model.
56+
* @returns A promise that resolves to the generated text string.
57+
*/
58+
async getResult(message: string): Promise<string> {
59+
try {
60+
const result = await this.textModel.generateContent(message)
61+
const response = await result.response
62+
63+
console.log({
64+
question: message,
65+
response: response.text(),
66+
})
67+
return response.text()
68+
} catch (error) {
69+
console.error("Failed to generate text from model:", error)
70+
return ""
71+
}
72+
}
73+
}

src/background/services/prompts.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
const OPTIONS_SEPARATOR = "|"
2+
3+
function wrapFormDataPoint(obj: object, tag: string): string {
4+
const start = `<${tag}>`
5+
const end = `</${tag}>`
6+
const dataString = Object.entries(obj)
7+
.map(([key, val]) => {
8+
if (Array.isArray(val)) {
9+
return `${key}: ${val.join(OPTIONS_SEPARATOR)}`
10+
}
11+
12+
return `${key}: ${val}`
13+
})
14+
.join("\n")
15+
return [start, dataString, end].join("\n")
16+
}
17+
18+
function wrapFormDataPoints(
19+
obj: object[],
20+
tag: string = "FormDataPoint",
21+
): string {
22+
return obj.map((o) => wrapFormDataPoint(o, tag)).join("\n\n")
23+
}
24+
25+
export function safeParseLLMJson(outputJson: string) {
26+
try {
27+
const json = JSON.parse(outputJson)
28+
return json
29+
} catch (err) {
30+
console.error(err)
31+
return null
32+
}
33+
}
34+
35+
export function wrapContext(
36+
context: {
37+
label: string
38+
value: string
39+
}[],
40+
) {
41+
const start = "<Context>"
42+
const end = "</Context>"
43+
const contextString = context.reduce((prev, { label, value }) => {
44+
return prev + `${label}:${value}\n`
45+
}, "")
46+
return [start, contextString, end].join("\n")
47+
}
48+
49+
export function buildTextInputDataExtractionPrompt(
50+
contextPrompt: string,
51+
dataPoints: object[],
52+
) {
53+
const dataPointText = wrapFormDataPoints(dataPoints)
54+
55+
return `
56+
You are an intelligent LLM that extracts data from context based on form input.
57+
Strictly respond with answer or null. Do not provide explanations.
58+
59+
${contextPrompt}
60+
61+
Provided below is extracted form input data, you have to pick the correct value from context
62+
based on title, placeholder and label of form. If they are empty, try to find value based on
63+
closestLabel and closestText. Convert answer in any format if it's mentioned.
64+
If type is textarea, answer with statements. If type in input, answer accurately and precisely.
65+
66+
67+
${dataPointText}
68+
`
69+
}
70+
71+
export function buildSingleSelectInputDataExtractionPrompt(
72+
contextPrompt: string,
73+
dataPoints: object[],
74+
): string {
75+
const dataPointText = wrapFormDataPoints(dataPoints, "SingleChoice")
76+
77+
return `
78+
You are an intelligent LLM that extracts data from context based on form input.
79+
Strictly respond with correct option or null. Do not provide explanations.
80+
81+
${contextPrompt}
82+
83+
Provided below is list of single choice questions <SingleChoice>.
84+
The question is present in title, placeholder, label, closestLabel or closestText.
85+
The options are separated by ${OPTIONS_SEPARATOR} character. Based on provided context and question,
86+
pick only one correct option, return null if the answer of question is not present
87+
in context or option.
88+
89+
${dataPointText}
90+
`
91+
}
92+
93+
export function buildMultiSelectInputDataExtractionPrompt(
94+
contextPrompt: string,
95+
dataPoints: object[],
96+
): string {
97+
const dataPointText = wrapFormDataPoints(dataPoints, "MultipleChoice")
98+
99+
return `
100+
You are an intelligent LLM that extracts data from context based on form input.
101+
Strictly respond with correct option or null. Do not provide explanations.
102+
103+
${contextPrompt}
104+
105+
Provided below is list of multiple choice questions <MultipleChoice>.
106+
The question is present in title, placeholder, label, closestLabel or closestText.
107+
The options are separated by ${OPTIONS_SEPARATOR} character.
108+
Based on provided context and question, return an array of options
109+
that are applicable for the questions, return null if no options apply for the
110+
context.
111+
112+
${dataPointText}
113+
`
114+
}

src/background/services/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface AccurateExtractResult {
2+
dataId: string
3+
value: string
4+
}

0 commit comments

Comments
 (0)