mirror of
https://github.com/msoedov/agentic_security.git
synced 2026-06-24 06:09:55 +02:00
628 lines
26 KiB
HTML
628 lines
26 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LLM Vulnerability Scanner</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/vue@2.6.12/dist/vue.js"></script>
|
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
p0: "#a18072",
|
|
clifford: '#da373d',
|
|
soft: "#f5f5f5",
|
|
"earthy-zen": "#61aaf2",
|
|
accent: "#4d4c7d",
|
|
alizarin: {
|
|
'50': '#fef2f2',
|
|
'100': '#fde3e4',
|
|
'200': '#fdcbcd',
|
|
'300': '#faa7aa',
|
|
'400': '#f57479',
|
|
'500': '#eb484e',
|
|
'600': '#da373d',
|
|
'700': '#b52025',
|
|
'800': '#961e22',
|
|
'900': '#7d1f22',
|
|
'950': '#440b0d',
|
|
},
|
|
earth: {
|
|
1: "#1b1b2f",
|
|
2: "#1b1b2f",
|
|
3: "#1b1b2f",
|
|
4: "#1b1b2f",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="bg-soft p-8">
|
|
<!-- Vue app root element -->
|
|
<div id="vue-app">
|
|
<h4
|
|
class="-mx-20 px-24 text-center bg-earthy-zen py-4 text-l text-white text-dark-primary ">🚀
|
|
NEW: Star Langalf on <a
|
|
href="https://github.com/msoedov/langalf"
|
|
target="_blank"
|
|
class="text-dark-primary underline"
|
|
data-faitracker-click-bind="true">Github</a> 🚀</h4>
|
|
<div
|
|
class="header flex items-center justify-between px-4 py-3 text-earth-1 bg-background ">
|
|
<div class="header__title flex items-center">
|
|
<i class="text-earth-1" data-lucide="triangle"></i>
|
|
</div>
|
|
<div class="header__actions flex items-center space-x-4">
|
|
<a href="https://github.com/msoedov/langalf" target="_blank"
|
|
rel="noreferrer"
|
|
class="github-link flex items-center gap-4 hover:text-accent focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent"
|
|
aria-label="Star on GitHub">
|
|
<svg aria-hidden="true" focusable="false" class="h-6 w-6"
|
|
fill="currentColor" viewBox="0 0 496 512"><path
|
|
d="..."></path></svg>
|
|
<span class="hidden lg:inline">Docs</span>
|
|
</a>
|
|
<!-- <a href="https://github.com/msoedov/langalf" target="_blank"
|
|
rel="noreferrer"
|
|
class="github-link flex items-center gap-4 hover:text-accent focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent"
|
|
aria-label="Star on GitHub">
|
|
<svg aria-hidden="true" focusable="false" class="h-6 w-6"
|
|
fill="currentColor" viewBox="0 0 496 512"><path
|
|
d="..."></path></svg>
|
|
<span class="hidden lg:inline">Github</span>
|
|
<i data-lucide="github">I</i>
|
|
</a> -->
|
|
</div>
|
|
</div>
|
|
|
|
<main class="flex flex-col gap-4 p-4 ">
|
|
<div
|
|
class="rounded-lg border bg-card text-card-foreground shadow-sm"
|
|
data-v0-t="card">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<h3
|
|
class="text-2xl md:text-3xl font-bold tracking-tight leading-none text-center my-2">
|
|
Agentic LLM Vulnerability Scanner
|
|
<span
|
|
class="text-xl font-semibold ml-2 px-2 py-1 rounded-full bg-earth-1 text-gray-100"
|
|
aria-label="Beta Version" style="vertical-align: middle;">
|
|
[Beta]
|
|
</span>
|
|
</h3>
|
|
|
|
<p class="text-sm text-muted-foreground text-center ">Input the API
|
|
LLM spec
|
|
and specify the maximum budget in tokens.</p>
|
|
</div>
|
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex flex-col space-y-4">
|
|
<div class="text-lg font-semibold">Select a config</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div v-for="(config, index) in configs" :key="index"
|
|
@click="selectConfig(index)"
|
|
class="border-2 rounded-lg p-4 flex flex-col items-start transition-all hover:shadow-md"
|
|
:class="{'border-earth-1': selectedConfig === index, 'border-gray-300': selectedConfig !== index}">
|
|
<div class="flex items-center justify-between w-full">
|
|
<div class="font-medium"
|
|
:class="{'text-earth-1': selectedConfig === index, 'text-gray-800': selectedConfig !== index}">
|
|
{{ config.name }}
|
|
</div>
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
:class="{'text-earth-1': selectedConfig === index, 'text-gray-600': selectedConfig !== index}">
|
|
<path stroke-linecap="round" stroke-linejoin="round"
|
|
stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<div class="text-sm text-gray-600">{{config.customInstructions
|
|
|| 'Requires API key'}}</div>
|
|
<div class="mt-2 text-gray-800 font-semibold">API</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
<div class="grid gap-4">
|
|
<div class="grid gap-1.5">
|
|
<label
|
|
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
for="llm-spec">
|
|
LLM API Spec, PROMPT variable will be replaced with the
|
|
testing prompt
|
|
|
|
</label>
|
|
<textarea
|
|
class="border-input shadow appearance-none border custom-textarea rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
id="llm-spec"
|
|
v-model="modelSpec"
|
|
@input="adjustHeight"></textarea>
|
|
</div>
|
|
<div class="grid gap-1.5">
|
|
<label
|
|
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
for="max-budget">
|
|
Maximum Budget in {{budget}}M Tokens
|
|
</label>
|
|
<input
|
|
class="flex h-10 w-full rounded-md border border-earth-disabled bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
id="max-budget"
|
|
placeholder="Enter maximum budget..."
|
|
type="number"
|
|
v-model="budget" />
|
|
</div>
|
|
<div
|
|
class="rounded-lg text-card-foreground shadow-sm mt-10 mb-10 border border-gray-300">
|
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 mt-5 mb-5">
|
|
<div class="flex flex-col space-y-4">
|
|
<!-- Accordion Header -->
|
|
<button
|
|
@click="toggleDatasets"
|
|
class="flex justify-between items-center text-lg font-semibold w-full py-2 text-center">
|
|
Modules [{{selectedDS}}]
|
|
selected
|
|
<svg
|
|
:class="{'rotate-180': showDatasets}"
|
|
class="h-5 w-5 transform transition-transform duration-200"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- Accordion Content -->
|
|
<div
|
|
v-show="showDatasets"
|
|
class="grid grid-cols-1 md:grid-cols-4 gap-4 transition-all duration-500 ">
|
|
<div
|
|
v-for="(package, index) in dataConfig"
|
|
:key="index"
|
|
@click="addPackage(index)"
|
|
class="border-2 rounded-lg p-4 flex flex-col items-start hover:shadow-md transition-all"
|
|
:class="{'border-earth-1': package.selected, 'border-gray-200': !package.selected}">
|
|
<div class="flex items-center justify-between w-full">
|
|
<div
|
|
class="font-medium"
|
|
:class="{'text-earth-1': package.selected, 'text-gray-800': !package.selected}">
|
|
{{ package.dataset_name }}
|
|
</div>
|
|
<svg
|
|
class="h-5 w-5"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
:class="{'text-earth-1': package.selected, 'text-gray-600': !package.selected}">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<div class="text-sm text-gray-600">
|
|
{{ package.source || 'Local dataset' }}
|
|
</div>
|
|
<div class="mt-2 text-gray-800 font-semibold"
|
|
v-if="!package.dynamic">
|
|
{{ package.num_prompts.toLocaleString() }} prompts
|
|
</div>
|
|
<div class="mt-2 text-gray-800 font-semibold"
|
|
v-if="package.dynamic">
|
|
Dynamic dataset
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
|
|
role="alert" v-if="errorMsg">
|
|
<strong class="font-bold">Oops!</strong>
|
|
<span class="block sm:inline">{{errorMsg}}</span>
|
|
<span class="absolute top-0 bottom-0 right-0 px-4 py-3">
|
|
<svg class="fill-current h-6 w-6 text-red-500" role="button"
|
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
<title>Close</title>
|
|
<path
|
|
d="M14.348 14.849a1.02 1.02 0 0 1-1.414 0L10 11.414 7.656 13.758a1.02 1.02 0 0 1-1.414 0 1.02 1.02 0 0 1 0-1.414l2.344-2.344-2.344-2.344a1.02 1.02 0 1 1 1.414-1.414L10 8.586l2.344-2.344a1.02 1.02 0 1 1 1.414 1.414L11.414 10l2.344 2.344a1.02 1.02 0 0 1 0 1.414z" />
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
<div
|
|
class="border-accent text-earth-2 px-4 py-3 rounded relative"
|
|
role="alert" v-if="okMsg">
|
|
<strong class="font-bold">></strong>
|
|
|
|
<span class="block sm:inline">{{okMsg}}</span>
|
|
<span class="absolute top-0 bottom-0 right-0 px-4 py-3">
|
|
<svg class="fill-current h-6 w-6 text-earth-2" role="button"
|
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
<title>Close</title>
|
|
<path
|
|
d="M14.348 14.849a1.02 1.02 0 0 1-1.414 0L10 11.414 7.656 13.758a1.02 1.02 0 0 1-1.414 0 1.02 1.02 0 0 1 0-1.414l2.344-2.344-2.344-2.344a1.02 1.02 0 1 1 1.414-1.414L10 8.586l2.344-2.344a1.02 1.02 0 1 1 1.414 1.414L11.414 10l2.344 2.344a1.02 1.02 0 0 1 0 1.414z" />
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex gap-4">
|
|
|
|
<button
|
|
@click="verifyIntegration"
|
|
class="inline-flex items-center text-gray-100 justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-earth-1 text-earth-foreground hover:bg-earth-1/90 h-10 px-4 py-2">
|
|
Verify Integration
|
|
|
|
</button>
|
|
<button
|
|
@click="startScan"
|
|
class="inline-flex text-gray-100 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-earth-1 text-earth-foreground hover:bg-earth-1/90 h-10 px-4 py-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" stroke-width="2"
|
|
stroke-linecap="round" stroke-linejoin="round"
|
|
class="lucide lucide-arrow-right mr-1"><path
|
|
d="M5 12h14"></path><path
|
|
d="m12 5 7 7-7 7"></path></svg>
|
|
Run Scan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="progress"
|
|
class="w-24 bg-earth-1 rounded-full h-2 overflow-hidden"
|
|
v-bind:style="{width: progressWidth}">
|
|
|
|
</div>
|
|
<img :src="imageUrl" alt="Generated Plot">
|
|
<div
|
|
class="rounded-lg border bg-card text-card-foreground shadow-sm"
|
|
data-v0-t="card">
|
|
<div class="flex flex-col space-y-1.5 p-6">
|
|
<h3
|
|
class="text-2xl font-semibold whitespace-nowrap leading-none tracking-tight">Scan
|
|
Results</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<div class="relative w-full overflow-auto">
|
|
<table class="w-full caption-bottom text-sm">
|
|
<thead class="[&_tr]:border-b">
|
|
<tr
|
|
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted">
|
|
<th
|
|
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0">
|
|
Vulnerability Module
|
|
</th>
|
|
<th
|
|
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0">
|
|
% Protection rate
|
|
</th>
|
|
<th
|
|
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0">
|
|
Number of Tokens
|
|
</th>
|
|
<th
|
|
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0">
|
|
Cost (in gpt-3 tokens)
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="[&_tr:last-child]:border-0">
|
|
<tr v-for="result in mainTable"
|
|
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
|
|
:class="{'text-accent': result.last, 'text-gray-800': !result.last}">
|
|
|
|
<td
|
|
class="p-4 align-middle [&:has([role=checkbox])]:pr-0">{{result.module}}</td>
|
|
<td
|
|
class="p-4 align-middle [&:has([role=checkbox])]:pr-0"
|
|
:class="getFailureRateColor(result.failureRate)">{{(100
|
|
- result.failureRate).toFixed(2)}}</td>
|
|
<td
|
|
class="p-4 align-middle [&:has([role=checkbox])]:pr-0">{{result.tokens}}k</td>
|
|
<td
|
|
class="p-4 align-middle [&:has([role=checkbox])]:pr-0">${{result.cost.toFixed(2)}}</td>
|
|
</tr>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
@click="downloadFailures"
|
|
class="inline-flex text-gray-100 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-earth-1 text-earth-foreground hover:bg-earth-1/90 h-10 px-4 py-2">
|
|
Download failures
|
|
</button>
|
|
|
|
</main>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
let URL = window.location.href;
|
|
if (URL.endsWith('/')) {
|
|
URL = URL.slice(0, -1);
|
|
}
|
|
|
|
// Vue application
|
|
let LLM_SPECS = [
|
|
`POST ${URL}/v1/self-probe
|
|
Authorization: Bearer XXXXX
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"prompt": "<<PROMPT>>"
|
|
}
|
|
|
|
`,
|
|
`POST https://api.openai.com/v1/chat/completions
|
|
Authorization: Bearer sk-xxxxxxxxx
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"model": "gpt-3.5-turbo",
|
|
"messages": [{"role": "user", "content": "<<PROMPT>>"}],
|
|
"temperature": 0.7
|
|
}
|
|
`,
|
|
`POST https://api.replicate.com/v1/models/mistralai/mixtral-8x7b-instruct-v0.1/predictions
|
|
Authorization: Bearer $APIKEY
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"input": {
|
|
"top_k": 50,
|
|
"top_p": 0.9,
|
|
"prompt": "Write a bedtime story about neural networks I can read to my toddler",
|
|
"temperature": 0.6,
|
|
"max_new_tokens": 1024,
|
|
"prompt_template": "<s>[INST] <<PROMPT>> [/INST] ",
|
|
"presence_penalty": 0,
|
|
"frequency_penalty": 0
|
|
}
|
|
}
|
|
`,
|
|
`POST https://api.groq.com/v1/request_manager/text_completion
|
|
Authorization: Bearer $APIKEY
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"model_id": "codellama-34b",
|
|
"system_prompt": "You are helpful and concise coding assistant",
|
|
"user_prompt": "<<PROMPT>>"
|
|
}
|
|
`,
|
|
]
|
|
var app = new Vue({
|
|
el: '#vue-app',
|
|
data: {
|
|
progressWidth: '0%',
|
|
modelSpec: LLM_SPECS[0],
|
|
budget: 50,
|
|
showDatasets: false,
|
|
scanResults: [],
|
|
mainTable: [],
|
|
integrationVerified: false,
|
|
scanRunning: false,
|
|
errorMsg: '',
|
|
maskMode: false,
|
|
okMsg: '',
|
|
reportImageUrl: '',
|
|
selectedConfig: 0,
|
|
configs: [
|
|
{ name: 'Custom API', prompts: 40000, customInstructions: 'Requires api spec' },
|
|
{ name: 'Open AI', prompts: 24000 },
|
|
{ name: 'Replicate', prompts: 40000 },
|
|
{ name: 'Groq', prompts: 40000 },
|
|
],
|
|
dataConfig: [],
|
|
},
|
|
mounted: function() {
|
|
console.log('Vue app mounted');
|
|
this.adjustHeight({ target: document.getElementById('llm-spec') });
|
|
// this.startScan();
|
|
this.loadConfigs();
|
|
},
|
|
computed : {
|
|
selectedDS: function() {
|
|
return this.dataConfig.filter(p => p.selected).length;
|
|
}
|
|
},
|
|
methods: {
|
|
downloadFailures() {
|
|
window.open('/failures', '_blank');
|
|
},
|
|
toggleDatasets() {
|
|
this.showDatasets = !this.showDatasets;
|
|
},
|
|
hide() {
|
|
this.maskMode = !this.maskMode;
|
|
},
|
|
verifyIntegration: async function() {
|
|
let payload = {
|
|
spec: this.modelSpec,
|
|
};
|
|
const response = await fetch(`${URL}/verify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
console.log(response);
|
|
let txt = await response.text();
|
|
if (!response.ok) {
|
|
this.errorMsg = 'Integration verification failed:' + txt;
|
|
} else {
|
|
this.errorMsg = '';
|
|
this.okMsg = 'Integration verified';
|
|
this.integrationVerified = true;
|
|
// console.log('Integration verified', this.integrationVerified);
|
|
// this.$forceUpdate();
|
|
|
|
}
|
|
},
|
|
loadConfigs: async function() {
|
|
const response = await fetch(`${URL}/v1/data-config`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
console.log(response);
|
|
this.dataConfig = await response.json();
|
|
},
|
|
selectConfig(index) {
|
|
this.selectedConfig = index;
|
|
this.modelSpec = LLM_SPECS[index];
|
|
this.adjustHeight({ target: document.getElementById('llm-spec') });
|
|
// this.adjustHeight({ target: document.getElementById('llm-spec') });
|
|
this.errorMsg = '';
|
|
this.integrationVerified = false;
|
|
|
|
},
|
|
addPackage(index) {
|
|
|
|
package = this.dataConfig[index];
|
|
package.selected = !package.selected;
|
|
|
|
},
|
|
getFailureRateColor(failureRate) {
|
|
// Uncomment the following line if you want to invert the failure rate
|
|
failureRate = 100 - failureRate;
|
|
if (failureRate >= 95) return 'bg-gray-100';
|
|
else if (failureRate >= 85) return 'bg-yellow-50';
|
|
else if (failureRate >= 75) return 'bg-yellow-50';
|
|
else if (failureRate >= 65) return 'bg-red-50';
|
|
else if (failureRate >= 55) return 'bg-red-100';
|
|
else if (failureRate >= 35) return 'bg-red-100';
|
|
else if (failureRate >= 25) return 'bg-red-200';
|
|
else if (failureRate >= 15) return 'bg-red-200';
|
|
else if (failureRate >= 10) return 'bg-red-200';
|
|
else if (failureRate >= 5) return 'bg-red-200';
|
|
else if (failureRate > 0) return 'bg-red-300';
|
|
else return 'bg-gray-800'; // This can be the default for failureRate of 0 or less
|
|
},
|
|
|
|
adjustHeight(event) {
|
|
const element = event.target;
|
|
// Reset height to ensure accurate measurement
|
|
element.style.height = 'auto';
|
|
// Adjust height based on scrollHeight
|
|
element.style.height = `${element.scrollHeight+100}px`;
|
|
},
|
|
newEvent: function(event) {
|
|
|
|
if (event.status) {
|
|
this.okMsg = `${event.module}`;
|
|
return
|
|
}
|
|
console.log('New event');
|
|
// { "module": "Module 49", "tokens": 480, "cost": 4.800000000000001, "progress": 9.8 }
|
|
let progress = event.progress;
|
|
this.progressWidth = `${progress}%`;
|
|
|
|
if (this.mainTable.length < 1) {
|
|
this.mainTable.push(event);
|
|
event.last = true;
|
|
|
|
return
|
|
}
|
|
let last = this.mainTable[this.mainTable.length - 1];
|
|
if (last.module === event.module) {
|
|
last.tokens = event.tokens;
|
|
last.cost = event.cost;
|
|
last.progress = event.progress;
|
|
last.failureRate = event.failureRate;
|
|
} else {
|
|
last.last = false;
|
|
this.mainTable.push(event);
|
|
event.last = true;
|
|
// this.newRow()
|
|
}
|
|
this.okMsg = `New event: ${event.module}: ${event.progress}%`;
|
|
|
|
},
|
|
newRow: async function() {
|
|
console.log('New row');
|
|
let payload = {
|
|
table: this.mainTable,
|
|
};
|
|
const response = await fetch(`${URL}/plot.jpeg`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
// Convert image response to a data URL for the <img> src
|
|
const blob = await response.blob();
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(blob);
|
|
reader.onloadend = () => {
|
|
this.reportImageUrl = reader.result;
|
|
};
|
|
},
|
|
startScan: async function() {
|
|
let payload = {
|
|
maxBudget: this.budget,
|
|
llmSpec: this.modelSpec,
|
|
datasets: this.dataConfig,
|
|
};
|
|
const response = await fetch(`${URL}/scan`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
this.okMsg = 'Scan started';
|
|
this.mainTable = [];
|
|
const reader = response.body.getReader();
|
|
let receivedLength = 0; // received that many bytes at the moment
|
|
let chunks = []; // array of received binary chunks (comprises the body)
|
|
while(true) {
|
|
const {done, value} = await reader.read();
|
|
|
|
if (done) {
|
|
break;
|
|
}
|
|
|
|
chunks.push(value);
|
|
receivedLength += value.length;
|
|
|
|
const chunkAsString = new TextDecoder("utf-8").decode(value);
|
|
const chunkAsLines = chunkAsString.split('\n').filter(line => line.trim());
|
|
|
|
self = this;
|
|
chunkAsLines.forEach(line => {
|
|
try {
|
|
const result = JSON.parse(line);
|
|
self.scanResults.push(result);
|
|
self.newEvent(result);
|
|
} catch (e) {
|
|
console.error('Error parsing chunk:', e);
|
|
}
|
|
});
|
|
}}}
|
|
});
|
|
</script>
|
|
<script>
|
|
lucide.createIcons();
|
|
</script>
|
|
</body>
|
|
</html>
|