Add basic functionality

This commit is contained in:
2026-10-05 21:51:31 +02:00
parent 91fde51eea
commit e9a3144f00
21 changed files with 2239 additions and 277 deletions
+52
View File
@@ -1,5 +1,57 @@
<script setup lang="ts">
import { ref } from 'vue';
import Input from './components/Input.vue';
import {load, Store} from "@tauri-apps/plugin-store";
import { invoke } from '@tauri-apps/api/core';
const initialized = ref(false);
const ollama_url = ref("");
const ollama_api_key = ref("");
const home_page=ref(false);
async function submit(ev){
ev.preventDefault();
await store.set("url",ollama_url.value);
await store.set("api-key",ollama_api_key.value);
await store.save();
try {
await invoke("connect_ollama",{url: ollama_url.value, apiKey: ollama_api_key.value});
} catch(e){
window.alert("Verbindung nicht möglich: "+e);
return;
}
home_page.value=true;
}
let store: Store;
async function init(){
store = await load('store.json', { autoSave: false });
if (await store.has("url")){
ollama_url.value=await store.get("url");
}
if (await store.has("api-key")){
ollama_api_key.value=await store.get("api-key");
}
initialized.value=true;
}
init();
</script>
<template>
<main class="w-screen h-screen overflow-hidden flex items-center justify-center p-10" v-if="initialized">
<div class="w-full h-full border rounded-2xl transition-all ease-in-out duration-200 p-6 overflow-y-scroll">
<div v-if="!home_page">
<h1>Moin!</h1>
<p>Mit dieser App kannst du verschiedene Modelle über die Ollama API ausprobieren und vergleichen.</p>
<form @submit="submit">
<Input placeholder="Ollama URL (z.B. http://localhost:11434)" class="w-full" v-model="ollama_url"/> <br>
<Input placeholder="API Key (optional)" class="w-full" type="password" v-model="ollama_api_key"/> <br>
<br>
<input value="Weiter" class="w-full border" type="submit"/>
</form>
</div>
<RouterView v-else/>
</div>
</main>
</template>
+1 -1
View File
@@ -1 +1 @@
@import "tailwindcss";
@import "tailwindcss"
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<RouterLink to="/">Zurück zur Startseite</RouterLink>
<h1>Context Window einstellen</h1>
Das Context Window muss beim Start des Ollama Servers als Environment-Variable gesetzt werden. Mehr Informationen <a href="https://docs.ollama.com/context-length" target="_blank" class="underline text-blue-700">hier.</a>
</template>
+66
View File
@@ -0,0 +1,66 @@
<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { ref } from 'vue';
const initialized=ref(false);
const models=ref([]);
const model=ref("");
const new_model=ref("");
const information=ref(undefined);
const system_prompt=ref("");
async function init(){
models.value=await invoke("get_models");
initialized.value=true;
}
init();
async function load_model(localmodel){
model.value=localmodel;
let info;
try{
info=await invoke("get_model_info",{model: model.value});
} catch(e){
window.alert("Das funktioniert mit diesem Modell leider nicht.");
return
}
information.value={parameters: info.parameters, template: info.template}
console.log(information.value)
}
async function save(){
try {
window.alert("Antwort von Ollama: " + await invoke("create_model",{fromModel: model.value, template: information.value.template, systemPrompt: system_prompt.value, newModel: new_model.value}))
} catch(e){
window.alert("Das hat nicht geklappt. Fehlermeldung: "+e)
return
}
}
</script>
<template>
<RouterLink to="/">Zurück zur Startseite</RouterLink>
<h1>Modell bearbeiten</h1>
<p>Diese Funktion ist leider nicht für alle Modelle verfügbar.</p>
<div v-if="initialized">
<ul v-if="information==undefined">
<li @click="load_model(model.name)" v-for="model in models">{{ model.name }}</li>
</ul>
<form v-else @submit.prevent="save">
<button @click="information=undefined">Zurück zur Modellauswahl</button>
<textarea v-model="information.template" placeholder="Prompt Template" class="w-full"></textarea>
<textarea v-model="system_prompt" placeholder="System Prompt" class="w-full"></textarea>
<input type="text" placeholder="Neuer Modellname" v-model="new_model">
<input type="submit" value="Speichern"/>
</form>
</div>
<div v-else>
Rufe die Liste der Modelle bei Ollama ab...
</div>
</template>
+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { ref } from 'vue';
import EmbeddingVisualizer from './EmbeddingVisualizer.vue';
import PromptInput from './PromptInput.vue';
import { invoke } from '@tauri-apps/api/core';
const dataset=ref([]);
const model = ref("");
const model_options = ref([]);
const prompt = ref("");
async function execute_prompt(ev){
ev.preventDefault();
let embedding;
try{
embedding=await invoke("prompt_embedding",{model: model.value, prompt: prompt.value});
}catch(e){
window.alert("Fehler beim Ausführen des Prompts: "+e);
}
if(embedding!=undefined){
dataset.value.push({vector: embedding, label: prompt.value});
prompt.value="";
}
}
async function init(){
model_options.value=await invoke("get_models")
}
init();
</script>
<template>
<RouterLink to="/">Zurück zur Startseite</RouterLink>
<EmbeddingVisualizer :dataset="dataset"/>
<select class="w-[500px]" v-model="model">
<option value="" disabled></option>
<option :value="m.name" v-for="m in model_options">{{ m.name }}</option>
</select>
<button class="border ml-2" @click="dataset=[]">Punkte löschen</button>
<form @submit="execute_prompt">
<PromptInput v-model="prompt"/>
</form>
</template>
+153
View File
@@ -0,0 +1,153 @@
<script setup lang="ts">
import { UMAP } from 'umap-js';
import { onMounted, ref, useTemplateRef, watch } from "vue";
const canvas = useTemplateRef("canvas");
const {dataset} = defineProps({
dataset: {
type: Array<{vector: Array<number>, label: String}>,
required: true
}
});
// Simple PCA-like projection
function project(data) {
if (data.length === 0) return [];
const vectors = data.map((d) => d.vector);
const dims = vectors[0].length;
// Center the data
const means = Array(dims).fill(0);
vectors.forEach((v) => v.forEach((val, i) => (means[i] += val)));
means.forEach((m, i) => (means[i] = m / vectors.length));
const centered = vectors.map((v) => v.map((val, i) => val - means[i]));
// Simple projection using first two "principal" directions
// (This is a simplified version - real PCA would compute eigenvectors)
const proj = centered.map((v) => {
const x = v.reduce(
(sum, val, i) => sum + val * (i % 2 === 0 ? 1 : -0.5),
0,
);
const y = v.reduce(
(sum, val, i) => sum + val * (i % 2 === 1 ? 1 : -0.5),
0,
);
return [x, y];
});
// Normalize to [0, 1]
const minX = Math.min(...proj.map((p) => p[0]));
const maxX = Math.max(...proj.map((p) => p[0]));
const minY = Math.min(...proj.map((p) => p[1]));
const maxY = Math.max(...proj.map((p) => p[1]));
const rangeX = maxX - minX || 1;
const rangeY = maxY - minY || 1;
return proj.map((p) => [(p[0] - minX) / rangeX, (p[1] - minY) / rangeY]);
}
function draw(ctx, width, height, pointSize, showConnections, showLabels, points) {
let projected=project(points);
const padding = 50;
ctx.clearRect(0, 0, width, height);
// Draw grid
ctx.strokeStyle = document.documentElement.classList.contains("dark")
? "#374151"
: "#e5e7eb";
ctx.lineWidth = 1;
for (let i = 0; i <= 10; i++) {
const x = padding + ((width - 2 * padding) * i) / 10;
const y = padding + ((height - 2 * padding) * i) / 10;
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, height - padding);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
}
// Draw connections to nearest neighbors
if (showConnections && projected.length > 1) {
ctx.strokeStyle = "#94a3b8";
ctx.lineWidth = 1;
projected.forEach((p, i) => {
// Find nearest neighbor
let minDist = Infinity;
let nearest = -1;
projected.forEach((q, j) => {
if (i !== j) {
const dist = Math.hypot(p[0] - q[0], p[1] - q[1]);
if (dist < minDist) {
minDist = dist;
nearest = j;
}
}
});
if (nearest >= 0) {
const x1 = padding + p[0] * (width - 2 * padding);
const y1 = padding + p[1] * (height - 2 * padding);
const x2 = padding + projected[nearest][0] * (width - 2 * padding);
const y2 = padding + projected[nearest][1] * (height - 2 * padding);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
});
}
// Draw points
projected.forEach((p, i) => {
const x = padding + p[0] * (width - 2 * padding);
const y = padding + p[1] * (height - 2 * padding);
ctx.beginPath();
ctx.arc(x, y, pointSize, 0, Math.PI * 2);
ctx.fill();
// Draw labels
if (showLabels) {
ctx.fillStyle = document.documentElement.classList.contains("dark")
? "#e5e7eb"
: "#374151";
ctx.font = "12px Inter, sans-serif";
ctx.textAlign = "center";
ctx.fillText(points[i].label, x, y - pointSize - 5);
}
});
}
let ctx;
watch(()=>dataset, async (old_value, new_value)=>{
draw(ctx,500,500,3,false,true, new_value);
}, {deep: true});
onMounted(()=>{
ctx=canvas.value.getContext("2d");
draw(ctx,500,500,3,false,true, dataset);
});
</script>
<template>
<canvas ref="canvas" class="w-[500px] h-[500px] border border-gray-200 rounded-lg bg-gray-50" width="500" height="500"></canvas>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup lang="ts">
</script>
<template>
<main>
<h1>Home</h1>
<RouterLink to="/pull">Modelle runterladen</RouterLink> <br>
<RouterLink to="/embedding">Embedding-Modelle</RouterLink> <br>
<RouterLink to="/normal">"Normale" Modelle</RouterLink> <br>
<RouterLink to="/context">Context Window einstellen</RouterLink> <br>
<RouterLink to="/edit">Modell bearbeiten</RouterLink> <br>
</main>
</template>
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
const model=defineModel();
const {placeholder, type} = defineProps({
"placeholder": {
type: String
},
"type": {
type: String
}
});
</script>
<template>
<input :placeholder="placeholder" v-model="model" :type="type" class="border rounded-md my-1"/>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts" setup>
import Input from './Input.vue';
let {placeholder} = defineProps({
placeholder: {
type: String,
default: "Prompt"
}
});
</script>
<template>
<Input class="rounded-full w-full border-2 p-4 text-xl" :placeholder="placeholder"/>
</template>
+60
View File
@@ -0,0 +1,60 @@
<script setup lang="ts">
import { ref } from 'vue';
import PromptInput from './PromptInput.vue';
import { invoke } from '@tauri-apps/api/core';
const prompt=ref("");
const response=ref("");
const thinking_response=ref("");
const model=ref("");
const model_options=ref([]);
const thinking=ref(true);
const temperature=ref(0.2);
async function submit(ev){
ev.preventDefault();
response.value="Generiere eine Antwort..."
thinking_response.value="";
try {
let output:any=await invoke("prompt_normal",{model: model.value, prompt: prompt.value, thinking: thinking.value, temperature: temperature.value});
response.value=output.response;
thinking_response.value=output.thinking;
}catch(e){
window.alert("Fehler beim Generieren der Antwort: "+e);
}
prompt.value="";
}
async function init(){
model_options.value=await invoke("get_models");
}
init();
</script>
<template>
<RouterLink to="/">Zurück zur Startseite</RouterLink>
<br>
<br>
<i class="whitespace-pre-line">{{ thinking_response }}</i> <br>
<br>
<div class="whitespace-pre-line">{{ response }}</div>
<br>
<form @submit="submit">
<select class="w-[500px]" v-model="model">
<option value="" disabled></option>
<option :value="m.name" v-for="m in model_options">{{ m.name }}</option>
</select>
Thinking: <input type="checkbox" v-model="thinking">
Temperature: <input type="number" v-model="temperature" max="1" min="0" step="0.01" class="w-16 border">
<PromptInput v-model="prompt"/>
<input type="submit" value="Antwort generieren" class="border rounded-md p-2 cursor-pointer">
</form>
</template>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { openUrl } from '@tauri-apps/plugin-opener';
</script>
<template>
<RouterLink to="/">Zurück zur Startseite</RouterLink>
<br><br>
Gehe auf <span class="text-blue-600 underline cursor-pointer" @click="openUrl('https://ollama.com/search')">https://ollama.com/search</span> und suche dir das Modell, was du haben möchtest, raus. <br>
Dann öffne ein Terminal und führe "ollama pull {modellname}" aus.
</template>
+4 -1
View File
@@ -1,5 +1,8 @@
import { createApp } from "vue";
import App from "./App.vue";
import "./assets/style.css"
import router from "./router"
createApp(App).mount("#app");
createApp(App)
.use(router)
.mount("#app");
+24
View File
@@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../components/HomeView.vue'
import EmbeddingPrompt from '../components/EmbeddingPrompt.vue'
import PullModelsView from '../components/PullModelsView.vue'
import PromptView from '../components/PromptView.vue'
import ContextView from '../components/ContextView.vue'
import EditView from '../components/EditView.vue'
const routes = [
{ path: '/', component: HomeView },
{ path: '/normal', component: PromptView },
{ path: '/pull', component: PullModelsView },
{ path: '/embedding', component: EmbeddingPrompt },
{ path: '/context', component: ContextView },
{ path: '/edit', component: EditView },
]
const router = createRouter({
history: createWebHistory(),
routes,
})
export default router