Spaces:
Sleeping
Sleeping
Upload 14 files
Browse files- plugins/analytics.js +53 -0
- plugins/example-logger.js +64 -0
- plugins/storage.js +81 -0
- public/index.html +34 -0
- scripts/create-plugin.js +63 -0
- src/App.css +191 -0
- src/App.jsx +169 -0
- src/components/ChatBox.css +251 -0
- src/components/ChatBox.jsx +122 -0
- src/components/PluginLoader.jsx +86 -0
- src/index.css +89 -0
- src/main.jsx +16 -0
- src/utils/api.js +118 -0
- src/utils/pluginManager.js +53 -0
plugins/analytics.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Example Plugin: Analytics
|
| 3 |
+
*
|
| 4 |
+
* Dieses Plugin tracked User-Behavior und sammelt Stats
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
const analytics = {
|
| 8 |
+
sessionStart: null,
|
| 9 |
+
messagesCount: 0,
|
| 10 |
+
totalTokens: 0,
|
| 11 |
+
totalTime: 0,
|
| 12 |
+
averageResponseTime: 0,
|
| 13 |
+
messageHistory: [],
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function onPluginInit(context) {
|
| 17 |
+
analytics.sessionStart = new Date()
|
| 18 |
+
context.log('📊 Analytics Plugin started')
|
| 19 |
+
context.log(`Session ID: ${analytics.sessionStart.getTime()}`)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export function onMessageSent(context, { message, systemPrompt }) {
|
| 23 |
+
analytics.messagesCount++
|
| 24 |
+
analytics.messageHistory.push({
|
| 25 |
+
type: 'user',
|
| 26 |
+
content: message,
|
| 27 |
+
timestamp: new Date(),
|
| 28 |
+
})
|
| 29 |
+
|
| 30 |
+
context.log(`Message #${analytics.messagesCount}`)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
export function onResponseReceived(context, { content, stats }) {
|
| 34 |
+
analytics.totalTokens += stats.tokens
|
| 35 |
+
analytics.totalTime += stats.time
|
| 36 |
+
analytics.averageResponseTime = analytics.totalTime / analytics.messagesCount
|
| 37 |
+
|
| 38 |
+
analytics.messageHistory.push({
|
| 39 |
+
type: 'assistant',
|
| 40 |
+
content: content.substring(0, 100),
|
| 41 |
+
timestamp: new Date(),
|
| 42 |
+
tokens: stats.tokens,
|
| 43 |
+
responseTime: stats.time,
|
| 44 |
+
})
|
| 45 |
+
|
| 46 |
+
context.log(`📈 Stats:`)
|
| 47 |
+
context.log(` Messages: ${analytics.messagesCount}`)
|
| 48 |
+
context.log(` Total Tokens: ${analytics.totalTokens}`)
|
| 49 |
+
context.log(` Avg Time: ${analytics.averageResponseTime.toFixed(2)}s`)
|
| 50 |
+
|
| 51 |
+
// Mache diese Daten global verfügbar
|
| 52 |
+
window.analytics = analytics
|
| 53 |
+
}
|
plugins/example-logger.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Example Plugin: Logger
|
| 3 |
+
*
|
| 4 |
+
* Dieses Plugin zeigt, wie man ein einfaches Plugin schreibt
|
| 5 |
+
* das Messages und Responses logt.
|
| 6 |
+
*
|
| 7 |
+
* Um Plugins zu schreiben:
|
| 8 |
+
* 1. Neue JS-Datei im /plugins Ordner erstellen
|
| 9 |
+
* 2. Export der Hook-Funktionen (onPluginInit, onMessageSent, etc.)
|
| 10 |
+
* 3. Plugin wird automatisch geladen!
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
// Wird aufgerufen, wenn das Plugin initialisiert wird
|
| 14 |
+
export function onPluginInit(context) {
|
| 15 |
+
context.log('Example Logger Plugin initialized! 📝')
|
| 16 |
+
|
| 17 |
+
// Du kannst hier verschiedene Dinge machen:
|
| 18 |
+
// - State speichern
|
| 19 |
+
// - Event Listener registrieren
|
| 20 |
+
// - Daten fetchen
|
| 21 |
+
|
| 22 |
+
// Beispiel: Speichere Session-Start
|
| 23 |
+
window.pluginState = window.pluginState || {}
|
| 24 |
+
window.pluginState.sessionStart = new Date()
|
| 25 |
+
window.pluginState.messageCount = 0
|
| 26 |
+
|
| 27 |
+
context.log(`Session started at: ${window.pluginState.sessionStart}`)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Wird aufgerufen, wenn der Benutzer eine Nachricht sendet
|
| 31 |
+
export function onMessageSent(context, detail) {
|
| 32 |
+
const { message, systemPrompt } = detail
|
| 33 |
+
|
| 34 |
+
window.pluginState.messageCount++
|
| 35 |
+
|
| 36 |
+
context.log(`📤 Message #${window.pluginState.messageCount}: "${message.substring(0, 50)}..."`)
|
| 37 |
+
|
| 38 |
+
// Beispiele für weitere Dinge, die man hier machen könnte:
|
| 39 |
+
// - Analytics tracken
|
| 40 |
+
// - Nachricht validieren
|
| 41 |
+
// - Benutzerdaten speichern
|
| 42 |
+
// - API calls machen
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Wird aufgerufen, wenn eine Antwort vom Model einkommt
|
| 46 |
+
export function onResponseReceived(context, detail) {
|
| 47 |
+
const { content, stats } = detail
|
| 48 |
+
|
| 49 |
+
context.log(`📥 Response received:`)
|
| 50 |
+
context.log(` - Length: ${content.length} characters`)
|
| 51 |
+
context.log(` - Tokens: ${stats.tokens}`)
|
| 52 |
+
context.log(` - Time: ${stats.time}s`)
|
| 53 |
+
|
| 54 |
+
// Beispiele für weitere Dinge:
|
| 55 |
+
// - Response analysieren
|
| 56 |
+
// - Sentiment analysieren
|
| 57 |
+
// - Qualität prüfen
|
| 58 |
+
// - In Datenbank speichern
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Optional: Weitere Hook-Funktionen die du definieren kannst:
|
| 62 |
+
// export function onError(context, error) { }
|
| 63 |
+
// export function onModelLoaded(context, model) { }
|
| 64 |
+
// export function onSettingsChanged(context, settings) { }
|
plugins/storage.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Example Plugin: Custom Logging
|
| 3 |
+
*
|
| 4 |
+
* Dieses Plugin speichert alle Messages in localStorage
|
| 5 |
+
* und kann sie später abrufen
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
const STORAGE_KEY = 'chatbot_history'
|
| 9 |
+
const MAX_HISTORY = 100
|
| 10 |
+
|
| 11 |
+
function getHistory() {
|
| 12 |
+
try {
|
| 13 |
+
const data = localStorage.getItem(STORAGE_KEY)
|
| 14 |
+
return data ? JSON.parse(data) : []
|
| 15 |
+
} catch (e) {
|
| 16 |
+
console.error('Failed to read history:', e)
|
| 17 |
+
return []
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
function saveHistory(history) {
|
| 22 |
+
try {
|
| 23 |
+
const limited = history.slice(-MAX_HISTORY)
|
| 24 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(limited))
|
| 25 |
+
} catch (e) {
|
| 26 |
+
console.error('Failed to save history:', e)
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export function onPluginInit(context) {
|
| 31 |
+
const history = getHistory()
|
| 32 |
+
context.log(`💾 Local Storage Plugin initialized with ${history.length} messages`)
|
| 33 |
+
|
| 34 |
+
// API für Plugins bereitstellen
|
| 35 |
+
window.HistoryAPI = {
|
| 36 |
+
getHistory: getHistory,
|
| 37 |
+
clearHistory: () => {
|
| 38 |
+
localStorage.removeItem(STORAGE_KEY)
|
| 39 |
+
context.log('✓ History cleared')
|
| 40 |
+
},
|
| 41 |
+
exportAsJSON: () => {
|
| 42 |
+
const data = {
|
| 43 |
+
exported: new Date(),
|
| 44 |
+
messages: getHistory(),
|
| 45 |
+
}
|
| 46 |
+
const json = JSON.stringify(data, null, 2)
|
| 47 |
+
const blob = new Blob([json], { type: 'application/json' })
|
| 48 |
+
const url = URL.createObjectURL(blob)
|
| 49 |
+
const a = document.createElement('a')
|
| 50 |
+
a.href = url
|
| 51 |
+
a.download = `chatbot-history-${Date.now()}.json`
|
| 52 |
+
a.click()
|
| 53 |
+
context.log('✓ History exported')
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export function onMessageSent(context, { message, systemPrompt }) {
|
| 59 |
+
const history = getHistory()
|
| 60 |
+
history.push({
|
| 61 |
+
type: 'user',
|
| 62 |
+
content: message,
|
| 63 |
+
timestamp: new Date(),
|
| 64 |
+
systemPrompt,
|
| 65 |
+
})
|
| 66 |
+
saveHistory(history)
|
| 67 |
+
context.log(`💾 Saved to local storage (${history.length} messages)`)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function onResponseReceived(context, { content, stats }) {
|
| 71 |
+
const history = getHistory()
|
| 72 |
+
history.push({
|
| 73 |
+
type: 'assistant',
|
| 74 |
+
content,
|
| 75 |
+
timestamp: new Date(),
|
| 76 |
+
tokens: stats.tokens,
|
| 77 |
+
time: stats.time,
|
| 78 |
+
})
|
| 79 |
+
saveHistory(history)
|
| 80 |
+
context.log(`💾 Response saved (${history.length} total messages)`)
|
| 81 |
+
}
|
public/index.html
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="de">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Zephyr-7B Chatbot</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
display: flex;
|
| 19 |
+
justify-content: center;
|
| 20 |
+
align-items: center;
|
| 21 |
+
padding: 20px;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#root {
|
| 25 |
+
width: 100%;
|
| 26 |
+
max-width: 1000px;
|
| 27 |
+
}
|
| 28 |
+
</style>
|
| 29 |
+
</head>
|
| 30 |
+
<body>
|
| 31 |
+
<div id="root"></div>
|
| 32 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 33 |
+
</body>
|
| 34 |
+
</html>
|
scripts/create-plugin.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Setup Script - Initialisiert ein neues Plugin
|
| 5 |
+
* Nutzung: node scripts/create-plugin.js my-awesome-plugin
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
const fs = require('fs')
|
| 9 |
+
const path = require('path')
|
| 10 |
+
|
| 11 |
+
const pluginName = process.argv[2]
|
| 12 |
+
|
| 13 |
+
if (!pluginName) {
|
| 14 |
+
console.error('❌ Fehler: Plugin-Name erforderlich')
|
| 15 |
+
console.log('Nutzung: node scripts/create-plugin.js my-plugin-name')
|
| 16 |
+
process.exit(1)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const pluginDir = path.join(__dirname, '..', 'plugins')
|
| 20 |
+
const pluginFile = path.join(pluginDir, `${pluginName}.js`)
|
| 21 |
+
|
| 22 |
+
if (fs.existsSync(pluginFile)) {
|
| 23 |
+
console.error(`❌ Plugin ${pluginName} existiert bereits!`)
|
| 24 |
+
process.exit(1)
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const template = `/**
|
| 28 |
+
* Plugin: ${pluginName}
|
| 29 |
+
*
|
| 30 |
+
* Automatisch generiert am: ${new Date().toISOString()}
|
| 31 |
+
*
|
| 32 |
+
* Hooks:
|
| 33 |
+
* - onPluginInit(context): Beim Plugin-Start
|
| 34 |
+
* - onMessageSent(context, data): Wenn Benutzer Nachricht sendet
|
| 35 |
+
* - onResponseReceived(context, data): Wenn Response kommt
|
| 36 |
+
*/
|
| 37 |
+
|
| 38 |
+
export function onPluginInit(context) {
|
| 39 |
+
context.log('🎉 ${pluginName} plugin initialized!')
|
| 40 |
+
|
| 41 |
+
// TODO: Plugin-Setup hier
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export function onMessageSent(context, { message, systemPrompt }) {
|
| 45 |
+
context.log(\`📤 Message: \${message.substring(0, 50)}...\`)
|
| 46 |
+
|
| 47 |
+
// TODO: Message-Handler
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export function onResponseReceived(context, { content, stats }) {
|
| 51 |
+
context.log(\`📥 Response: \${content.substring(0, 50)}...\`)
|
| 52 |
+
context.log(\`⏱️ Time: \${stats.time}s\`)
|
| 53 |
+
|
| 54 |
+
// TODO: Response-Handler
|
| 55 |
+
}
|
| 56 |
+
`
|
| 57 |
+
|
| 58 |
+
fs.writeFileSync(pluginFile, template)
|
| 59 |
+
|
| 60 |
+
console.log(`✅ Plugin erstellt: ${pluginFile}`)
|
| 61 |
+
console.log(`\n🚀 Das Plugin wird automatisch geladen wenn die App startet!`)
|
| 62 |
+
console.log(`\n📝 Template-Datei: ${pluginFile}`)
|
| 63 |
+
console.log(`\n💡 Tipp: Öffne die Datei und füge deine Logik hinzu!`)
|
src/App.css
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.app-container {
|
| 2 |
+
display: flex;
|
| 3 |
+
flex-direction: column;
|
| 4 |
+
height: 100vh;
|
| 5 |
+
background: var(--gray-50);
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.app-header {
|
| 9 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
| 10 |
+
color: white;
|
| 11 |
+
padding: 30px 20px;
|
| 12 |
+
text-align: center;
|
| 13 |
+
box-shadow: var(--shadow-md);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.app-header h1 {
|
| 17 |
+
font-size: 32px;
|
| 18 |
+
margin-bottom: 5px;
|
| 19 |
+
font-weight: 700;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.tagline {
|
| 23 |
+
font-size: 14px;
|
| 24 |
+
opacity: 0.9;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.app-content {
|
| 28 |
+
display: flex;
|
| 29 |
+
flex: 1;
|
| 30 |
+
gap: 20px;
|
| 31 |
+
padding: 20px;
|
| 32 |
+
max-width: 1400px;
|
| 33 |
+
margin: 0 auto;
|
| 34 |
+
width: 100%;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.sidebar {
|
| 38 |
+
width: 300px;
|
| 39 |
+
background: white;
|
| 40 |
+
border-radius: var(--border-radius);
|
| 41 |
+
padding: 20px;
|
| 42 |
+
box-shadow: var(--shadow-md);
|
| 43 |
+
overflow-y: auto;
|
| 44 |
+
height: fit-content;
|
| 45 |
+
max-height: calc(100vh - 200px);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.sidebar-section {
|
| 49 |
+
display: flex;
|
| 50 |
+
flex-direction: column;
|
| 51 |
+
gap: 15px;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.sidebar-section h3 {
|
| 55 |
+
color: var(--gray-900);
|
| 56 |
+
font-size: 16px;
|
| 57 |
+
margin-bottom: 10px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.sidebar-section label {
|
| 61 |
+
font-size: 13px;
|
| 62 |
+
color: var(--gray-600);
|
| 63 |
+
font-weight: 600;
|
| 64 |
+
text-transform: uppercase;
|
| 65 |
+
letter-spacing: 0.5px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.sidebar-section textarea {
|
| 69 |
+
width: 100%;
|
| 70 |
+
min-height: 100px;
|
| 71 |
+
padding: 10px;
|
| 72 |
+
border: 1px solid var(--gray-300);
|
| 73 |
+
border-radius: var(--border-radius);
|
| 74 |
+
font-size: 13px;
|
| 75 |
+
font-family: 'Monaco', 'Menlo', monospace;
|
| 76 |
+
resize: vertical;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.sidebar-section input[type="range"] {
|
| 80 |
+
width: 100%;
|
| 81 |
+
margin-top: 5px;
|
| 82 |
+
accent-color: var(--primary);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.stats-box {
|
| 86 |
+
background: var(--gray-50);
|
| 87 |
+
padding: 15px;
|
| 88 |
+
border-radius: var(--border-radius);
|
| 89 |
+
border-left: 4px solid var(--success);
|
| 90 |
+
margin-top: 10px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.stats-box h4 {
|
| 94 |
+
font-size: 13px;
|
| 95 |
+
color: var(--gray-900);
|
| 96 |
+
margin-bottom: 10px;
|
| 97 |
+
text-transform: uppercase;
|
| 98 |
+
letter-spacing: 0.5px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.stat-item {
|
| 102 |
+
display: flex;
|
| 103 |
+
justify-content: space-between;
|
| 104 |
+
font-size: 12px;
|
| 105 |
+
color: var(--gray-600);
|
| 106 |
+
margin: 5px 0;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.stat-item strong {
|
| 110 |
+
color: var(--gray-900);
|
| 111 |
+
font-weight: 600;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.plugins-box {
|
| 115 |
+
background: var(--gray-50);
|
| 116 |
+
padding: 15px;
|
| 117 |
+
border-radius: var(--border-radius);
|
| 118 |
+
border-left: 4px solid var(--warning);
|
| 119 |
+
margin-top: 10px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.plugins-box h4 {
|
| 123 |
+
font-size: 13px;
|
| 124 |
+
color: var(--gray-900);
|
| 125 |
+
margin-bottom: 10px;
|
| 126 |
+
text-transform: uppercase;
|
| 127 |
+
letter-spacing: 0.5px;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.plugins-box ul {
|
| 131 |
+
list-style: none;
|
| 132 |
+
font-size: 12px;
|
| 133 |
+
color: var(--gray-600);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.plugins-box li {
|
| 137 |
+
padding: 5px 0;
|
| 138 |
+
padding-left: 10px;
|
| 139 |
+
position: relative;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.plugins-box li::before {
|
| 143 |
+
content: '●';
|
| 144 |
+
position: absolute;
|
| 145 |
+
left: 0;
|
| 146 |
+
color: var(--warning);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.chat-container {
|
| 150 |
+
flex: 1;
|
| 151 |
+
display: flex;
|
| 152 |
+
flex-direction: column;
|
| 153 |
+
background: white;
|
| 154 |
+
border-radius: var(--border-radius);
|
| 155 |
+
box-shadow: var(--shadow-md);
|
| 156 |
+
overflow: hidden;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
@media (max-width: 1024px) {
|
| 160 |
+
.app-content {
|
| 161 |
+
flex-direction: column;
|
| 162 |
+
gap: 15px;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.sidebar {
|
| 166 |
+
width: 100%;
|
| 167 |
+
max-height: none;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.chat-container {
|
| 171 |
+
min-height: 400px;
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
@media (max-width: 768px) {
|
| 176 |
+
.app-header {
|
| 177 |
+
padding: 20px 15px;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.app-header h1 {
|
| 181 |
+
font-size: 24px;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.app-content {
|
| 185 |
+
padding: 10px;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.sidebar {
|
| 189 |
+
padding: 15px;
|
| 190 |
+
}
|
| 191 |
+
}
|
src/App.jsx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react'
|
| 2 |
+
import './App.css'
|
| 3 |
+
import ChatBox from './components/ChatBox'
|
| 4 |
+
import PluginLoader from './components/PluginLoader'
|
| 5 |
+
import { sendMessage, getStats } from './utils/api'
|
| 6 |
+
|
| 7 |
+
function App() {
|
| 8 |
+
const [messages, setMessages] = useState([])
|
| 9 |
+
const [loading, setLoading] = useState(false)
|
| 10 |
+
const [systemPrompt, setSystemPrompt] = useState('Du bist ein hilfsbereiter KI-Assistent.')
|
| 11 |
+
const [temperature, setTemperature] = useState(0.7)
|
| 12 |
+
const [topP, setTopP] = useState(0.9)
|
| 13 |
+
const [stats, setStats] = useState(null)
|
| 14 |
+
const [plugins, setPlugins] = useState([])
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
// Auf Plugin-Änderungen hören
|
| 18 |
+
window.addEventListener('pluginsLoaded', (e) => {
|
| 19 |
+
setPlugins(e.detail)
|
| 20 |
+
console.log('Plugins loaded:', e.detail)
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
return () => {
|
| 24 |
+
window.removeEventListener('pluginsLoaded', () => {})
|
| 25 |
+
}
|
| 26 |
+
}, [])
|
| 27 |
+
|
| 28 |
+
const handleSendMessage = async (message) => {
|
| 29 |
+
// Nachricht zum Chat hinzufügen
|
| 30 |
+
const userMessage = {
|
| 31 |
+
id: Date.now(),
|
| 32 |
+
role: 'user',
|
| 33 |
+
content: message,
|
| 34 |
+
timestamp: new Date(),
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
setMessages(prev => [...prev, userMessage])
|
| 38 |
+
setLoading(true)
|
| 39 |
+
|
| 40 |
+
try {
|
| 41 |
+
// Plugins benachrichtigen
|
| 42 |
+
window.dispatchEvent(new CustomEvent('messageSent', {
|
| 43 |
+
detail: { message, systemPrompt }
|
| 44 |
+
}))
|
| 45 |
+
|
| 46 |
+
const response = await sendMessage(message, systemPrompt, temperature, topP)
|
| 47 |
+
|
| 48 |
+
const assistantMessage = {
|
| 49 |
+
id: Date.now() + 1,
|
| 50 |
+
role: 'assistant',
|
| 51 |
+
content: response.response,
|
| 52 |
+
timestamp: new Date(),
|
| 53 |
+
stats: {
|
| 54 |
+
tokens: response.tokens,
|
| 55 |
+
time: response.time_seconds,
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
setMessages(prev => [...prev, assistantMessage])
|
| 60 |
+
setStats(response)
|
| 61 |
+
|
| 62 |
+
// Plugins über Antwort benachrichtigen
|
| 63 |
+
window.dispatchEvent(new CustomEvent('responseReceived', {
|
| 64 |
+
detail: assistantMessage
|
| 65 |
+
}))
|
| 66 |
+
|
| 67 |
+
} catch (error) {
|
| 68 |
+
console.error('Error:', error)
|
| 69 |
+
const errorMessage = {
|
| 70 |
+
id: Date.now() + 1,
|
| 71 |
+
role: 'assistant',
|
| 72 |
+
content: `❌ Fehler: ${error.message}`,
|
| 73 |
+
timestamp: new Date(),
|
| 74 |
+
isError: true,
|
| 75 |
+
}
|
| 76 |
+
setMessages(prev => [...prev, errorMessage])
|
| 77 |
+
} finally {
|
| 78 |
+
setLoading(false)
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return (
|
| 83 |
+
<div className="app-container">
|
| 84 |
+
<PluginLoader onPluginsLoad={setPlugins} />
|
| 85 |
+
|
| 86 |
+
<header className="app-header">
|
| 87 |
+
<h1>🤖 Zephyr-7B Chatbot</h1>
|
| 88 |
+
<p className="tagline">Powered by Hugging Face</p>
|
| 89 |
+
</header>
|
| 90 |
+
|
| 91 |
+
<div className="app-content">
|
| 92 |
+
<aside className="sidebar">
|
| 93 |
+
<div className="sidebar-section">
|
| 94 |
+
<h3>⚙️ Einstellungen</h3>
|
| 95 |
+
|
| 96 |
+
<label>System Prompt</label>
|
| 97 |
+
<textarea
|
| 98 |
+
value={systemPrompt}
|
| 99 |
+
onChange={(e) => setSystemPrompt(e.target.value)}
|
| 100 |
+
placeholder="Definiere die Rolle des Assistenten..."
|
| 101 |
+
disabled={loading}
|
| 102 |
+
/>
|
| 103 |
+
|
| 104 |
+
<label>
|
| 105 |
+
Temperatur: {temperature.toFixed(2)}
|
| 106 |
+
<input
|
| 107 |
+
type="range"
|
| 108 |
+
min="0"
|
| 109 |
+
max="2"
|
| 110 |
+
step="0.1"
|
| 111 |
+
value={temperature}
|
| 112 |
+
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
| 113 |
+
disabled={loading}
|
| 114 |
+
/>
|
| 115 |
+
</label>
|
| 116 |
+
|
| 117 |
+
<label>
|
| 118 |
+
Top P: {topP.toFixed(2)}
|
| 119 |
+
<input
|
| 120 |
+
type="range"
|
| 121 |
+
min="0"
|
| 122 |
+
max="1"
|
| 123 |
+
step="0.05"
|
| 124 |
+
value={topP}
|
| 125 |
+
onChange={(e) => setTopP(parseFloat(e.target.value))}
|
| 126 |
+
disabled={loading}
|
| 127 |
+
/>
|
| 128 |
+
</label>
|
| 129 |
+
|
| 130 |
+
{stats && (
|
| 131 |
+
<div className="stats-box">
|
| 132 |
+
<h4>📊 Letzte Antwort</h4>
|
| 133 |
+
<div className="stat-item">
|
| 134 |
+
<span>Tokens:</span>
|
| 135 |
+
<strong>{stats.tokens}</strong>
|
| 136 |
+
</div>
|
| 137 |
+
<div className="stat-item">
|
| 138 |
+
<span>Zeit:</span>
|
| 139 |
+
<strong>{stats.time_seconds}s</strong>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
)}
|
| 143 |
+
|
| 144 |
+
{plugins.length > 0 && (
|
| 145 |
+
<div className="plugins-box">
|
| 146 |
+
<h4>🔌 Plugins ({plugins.length})</h4>
|
| 147 |
+
<ul>
|
| 148 |
+
{plugins.map(plugin => (
|
| 149 |
+
<li key={plugin}>{plugin}</li>
|
| 150 |
+
))}
|
| 151 |
+
</ul>
|
| 152 |
+
</div>
|
| 153 |
+
)}
|
| 154 |
+
</div>
|
| 155 |
+
</aside>
|
| 156 |
+
|
| 157 |
+
<main className="chat-container">
|
| 158 |
+
<ChatBox
|
| 159 |
+
messages={messages}
|
| 160 |
+
onSendMessage={handleSendMessage}
|
| 161 |
+
loading={loading}
|
| 162 |
+
/>
|
| 163 |
+
</main>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
)
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
export default App
|
src/components/ChatBox.css
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.chatbox {
|
| 2 |
+
display: flex;
|
| 3 |
+
flex-direction: column;
|
| 4 |
+
height: 100%;
|
| 5 |
+
gap: 0;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.messages {
|
| 9 |
+
flex: 1;
|
| 10 |
+
overflow-y: auto;
|
| 11 |
+
padding: 20px;
|
| 12 |
+
display: flex;
|
| 13 |
+
flex-direction: column;
|
| 14 |
+
gap: 15px;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.empty-state {
|
| 18 |
+
display: flex;
|
| 19 |
+
flex-direction: column;
|
| 20 |
+
align-items: center;
|
| 21 |
+
justify-content: center;
|
| 22 |
+
gap: 20px;
|
| 23 |
+
text-align: center;
|
| 24 |
+
padding: 40px 20px;
|
| 25 |
+
color: var(--gray-600);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
.empty-icon {
|
| 29 |
+
font-size: 64px;
|
| 30 |
+
opacity: 0.6;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.empty-state h2 {
|
| 34 |
+
color: var(--gray-900);
|
| 35 |
+
font-size: 24px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.empty-state p {
|
| 39 |
+
color: var(--gray-500);
|
| 40 |
+
font-size: 14px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.suggestions {
|
| 44 |
+
display: flex;
|
| 45 |
+
flex-direction: column;
|
| 46 |
+
gap: 10px;
|
| 47 |
+
margin-top: 20px;
|
| 48 |
+
width: 100%;
|
| 49 |
+
max-width: 300px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.suggestion-btn {
|
| 53 |
+
background: var(--gray-100);
|
| 54 |
+
color: var(--gray-900);
|
| 55 |
+
border: 1px solid var(--gray-300);
|
| 56 |
+
padding: 10px 15px;
|
| 57 |
+
border-radius: 20px;
|
| 58 |
+
font-size: 13px;
|
| 59 |
+
text-align: left;
|
| 60 |
+
transition: all 0.2s ease;
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.suggestion-btn:hover {
|
| 65 |
+
background: var(--primary);
|
| 66 |
+
color: white;
|
| 67 |
+
border-color: var(--primary);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.message {
|
| 71 |
+
display: flex;
|
| 72 |
+
gap: 10px;
|
| 73 |
+
animation: slideIn 0.3s ease;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
@keyframes slideIn {
|
| 77 |
+
from {
|
| 78 |
+
opacity: 0;
|
| 79 |
+
transform: translateY(10px);
|
| 80 |
+
}
|
| 81 |
+
to {
|
| 82 |
+
opacity: 1;
|
| 83 |
+
transform: translateY(0);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.message.user {
|
| 88 |
+
justify-content: flex-end;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.message.assistant {
|
| 92 |
+
justify-content: flex-start;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.message.error {
|
| 96 |
+
opacity: 0.8;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.message-avatar {
|
| 100 |
+
font-size: 20px;
|
| 101 |
+
flex-shrink: 0;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.message-content {
|
| 105 |
+
display: flex;
|
| 106 |
+
flex-direction: column;
|
| 107 |
+
gap: 5px;
|
| 108 |
+
max-width: 70%;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.message.user .message-content {
|
| 112 |
+
align-items: flex-end;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.message-text {
|
| 116 |
+
padding: 12px 15px;
|
| 117 |
+
border-radius: 15px;
|
| 118 |
+
word-wrap: break-word;
|
| 119 |
+
line-height: 1.5;
|
| 120 |
+
font-size: 14px;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.message.user .message-text {
|
| 124 |
+
background: var(--primary);
|
| 125 |
+
color: white;
|
| 126 |
+
border-bottom-right-radius: 4px;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.message.assistant .message-text {
|
| 130 |
+
background: var(--gray-100);
|
| 131 |
+
color: var(--gray-900);
|
| 132 |
+
border-bottom-left-radius: 4px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.message.error .message-text {
|
| 136 |
+
background: var(--error);
|
| 137 |
+
color: white;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.message-stats {
|
| 141 |
+
font-size: 11px;
|
| 142 |
+
color: var(--gray-500);
|
| 143 |
+
padding: 0 15px;
|
| 144 |
+
text-align: right;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.message.user .message-stats {
|
| 148 |
+
text-align: right;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.message.assistant .message-stats {
|
| 152 |
+
text-align: left;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
/* Typing indicator */
|
| 156 |
+
.typing-indicator {
|
| 157 |
+
display: flex;
|
| 158 |
+
gap: 4px;
|
| 159 |
+
padding: 12px 15px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.typing-indicator span {
|
| 163 |
+
width: 6px;
|
| 164 |
+
height: 6px;
|
| 165 |
+
border-radius: 50%;
|
| 166 |
+
background: var(--gray-400);
|
| 167 |
+
animation: typing 1.4s infinite;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.typing-indicator span:nth-child(2) {
|
| 171 |
+
animation-delay: 0.2s;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.typing-indicator span:nth-child(3) {
|
| 175 |
+
animation-delay: 0.4s;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
@keyframes typing {
|
| 179 |
+
0%, 60%, 100% {
|
| 180 |
+
opacity: 0.5;
|
| 181 |
+
transform: translateY(0);
|
| 182 |
+
}
|
| 183 |
+
30% {
|
| 184 |
+
opacity: 1;
|
| 185 |
+
transform: translateY(-8px);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.input-area {
|
| 190 |
+
display: flex;
|
| 191 |
+
gap: 10px;
|
| 192 |
+
padding: 15px 20px;
|
| 193 |
+
border-top: 1px solid var(--gray-200);
|
| 194 |
+
background: var(--gray-50);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.input-area textarea {
|
| 198 |
+
flex: 1;
|
| 199 |
+
padding: 10px 12px;
|
| 200 |
+
border: 1px solid var(--gray-300);
|
| 201 |
+
border-radius: 8px;
|
| 202 |
+
font-size: 14px;
|
| 203 |
+
font-family: inherit;
|
| 204 |
+
resize: none;
|
| 205 |
+
max-height: 100px;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.input-area textarea:disabled {
|
| 209 |
+
opacity: 0.6;
|
| 210 |
+
cursor: not-allowed;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.send-btn {
|
| 214 |
+
background: var(--primary);
|
| 215 |
+
color: white;
|
| 216 |
+
border: none;
|
| 217 |
+
padding: 10px 20px;
|
| 218 |
+
border-radius: 8px;
|
| 219 |
+
font-size: 13px;
|
| 220 |
+
font-weight: 600;
|
| 221 |
+
cursor: pointer;
|
| 222 |
+
transition: all 0.2s ease;
|
| 223 |
+
align-self: flex-end;
|
| 224 |
+
white-space: nowrap;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.send-btn:hover:not(:disabled) {
|
| 228 |
+
background: var(--secondary);
|
| 229 |
+
transform: translateY(-2px);
|
| 230 |
+
box-shadow: var(--shadow-lg);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.send-btn:disabled {
|
| 234 |
+
opacity: 0.5;
|
| 235 |
+
cursor: not-allowed;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
@media (max-width: 768px) {
|
| 239 |
+
.message-content {
|
| 240 |
+
max-width: 85%;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.input-area {
|
| 244 |
+
flex-direction: column;
|
| 245 |
+
gap: 10px;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.send-btn {
|
| 249 |
+
align-self: stretch;
|
| 250 |
+
}
|
| 251 |
+
}
|
src/components/ChatBox.jsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from 'react'
|
| 2 |
+
import './ChatBox.css'
|
| 3 |
+
|
| 4 |
+
export default function ChatBox({ messages, onSendMessage, loading }) {
|
| 5 |
+
const [inputValue, setInputValue] = useState('')
|
| 6 |
+
const messagesEndRef = useRef(null)
|
| 7 |
+
const inputRef = useRef(null)
|
| 8 |
+
|
| 9 |
+
const scrollToBottom = () => {
|
| 10 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
scrollToBottom()
|
| 15 |
+
}, [messages])
|
| 16 |
+
|
| 17 |
+
const handleSend = () => {
|
| 18 |
+
if (inputValue.trim() && !loading) {
|
| 19 |
+
onSendMessage(inputValue)
|
| 20 |
+
setInputValue('')
|
| 21 |
+
setTimeout(() => inputRef.current?.focus(), 0)
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const handleKeyPress = (e) => {
|
| 26 |
+
if (e.key === 'Enter' && !e.shiftKey && !loading) {
|
| 27 |
+
e.preventDefault()
|
| 28 |
+
handleSend()
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return (
|
| 33 |
+
<div className="chatbox">
|
| 34 |
+
<div className="messages">
|
| 35 |
+
{messages.length === 0 ? (
|
| 36 |
+
<div className="empty-state">
|
| 37 |
+
<div className="empty-icon">🤖</div>
|
| 38 |
+
<h2>Willkommen!</h2>
|
| 39 |
+
<p>Starte eine Unterhaltung mit dem Zephyr-7B Modell</p>
|
| 40 |
+
<div className="suggestions">
|
| 41 |
+
<button
|
| 42 |
+
className="suggestion-btn"
|
| 43 |
+
onClick={() => {
|
| 44 |
+
setInputValue('Was ist Machine Learning?')
|
| 45 |
+
inputRef.current?.focus()
|
| 46 |
+
}}
|
| 47 |
+
>
|
| 48 |
+
Was ist Machine Learning?
|
| 49 |
+
</button>
|
| 50 |
+
<button
|
| 51 |
+
className="suggestion-btn"
|
| 52 |
+
onClick={() => {
|
| 53 |
+
setInputValue('Erkläre Quantencomputing')
|
| 54 |
+
inputRef.current?.focus()
|
| 55 |
+
}}
|
| 56 |
+
>
|
| 57 |
+
Erkläre Quantencomputing
|
| 58 |
+
</button>
|
| 59 |
+
<button
|
| 60 |
+
className="suggestion-btn"
|
| 61 |
+
onClick={() => {
|
| 62 |
+
setInputValue('Schreibe einen Witz')
|
| 63 |
+
inputRef.current?.focus()
|
| 64 |
+
}}
|
| 65 |
+
>
|
| 66 |
+
Schreibe einen Witz
|
| 67 |
+
</button>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
) : (
|
| 71 |
+
messages.map((msg) => (
|
| 72 |
+
<div key={msg.id} className={`message ${msg.role} ${msg.isError ? 'error' : ''}`}>
|
| 73 |
+
<div className="message-avatar">
|
| 74 |
+
{msg.role === 'user' ? '👤' : '🤖'}
|
| 75 |
+
</div>
|
| 76 |
+
<div className="message-content">
|
| 77 |
+
<div className="message-text">{msg.content}</div>
|
| 78 |
+
{msg.stats && (
|
| 79 |
+
<div className="message-stats">
|
| 80 |
+
⏱️ {msg.stats.time}s • 📊 {msg.stats.tokens} tokens
|
| 81 |
+
</div>
|
| 82 |
+
)}
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
))
|
| 86 |
+
)}
|
| 87 |
+
{loading && (
|
| 88 |
+
<div className="message assistant loading">
|
| 89 |
+
<div className="message-avatar">🤖</div>
|
| 90 |
+
<div className="message-content">
|
| 91 |
+
<div className="typing-indicator">
|
| 92 |
+
<span></span>
|
| 93 |
+
<span></span>
|
| 94 |
+
<span></span>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
)}
|
| 99 |
+
<div ref={messagesEndRef} />
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<div className="input-area">
|
| 103 |
+
<textarea
|
| 104 |
+
ref={inputRef}
|
| 105 |
+
value={inputValue}
|
| 106 |
+
onChange={(e) => setInputValue(e.target.value)}
|
| 107 |
+
onKeyPress={handleKeyPress}
|
| 108 |
+
placeholder="Schreibe deine Frage hier... (Shift+Enter für Zeilenumbruch)"
|
| 109 |
+
disabled={loading}
|
| 110 |
+
rows="3"
|
| 111 |
+
/>
|
| 112 |
+
<button
|
| 113 |
+
className="send-btn"
|
| 114 |
+
onClick={handleSend}
|
| 115 |
+
disabled={loading || !inputValue.trim()}
|
| 116 |
+
>
|
| 117 |
+
{loading ? '⏳ Denke...' : '➤ Senden'}
|
| 118 |
+
</button>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
)
|
| 122 |
+
}
|
src/components/PluginLoader.jsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* PluginLoader Component
|
| 5 |
+
* Lädt automatisch alle JS-Plugins aus dem /plugins Ordner
|
| 6 |
+
*
|
| 7 |
+
* Jedes Plugin kann folgende Hooks nutzen:
|
| 8 |
+
* - onPluginInit(pluginContext)
|
| 9 |
+
* - onMessageSent(event)
|
| 10 |
+
* - onResponseReceived(event)
|
| 11 |
+
*/
|
| 12 |
+
export default function PluginLoader({ onPluginsLoad }) {
|
| 13 |
+
const [loadedCount, setLoadedCount] = useState(0)
|
| 14 |
+
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
loadPlugins()
|
| 17 |
+
}, [])
|
| 18 |
+
|
| 19 |
+
const loadPlugins = async () => {
|
| 20 |
+
try {
|
| 21 |
+
// Import alle JS-Dateien aus dem plugins Ordner
|
| 22 |
+
// Für Vite: Nutze import.meta.glob
|
| 23 |
+
const pluginModules = import.meta.glob('/plugins/*.js', { eager: false })
|
| 24 |
+
|
| 25 |
+
const loadedPlugins = []
|
| 26 |
+
|
| 27 |
+
for (const [path, importFn] of Object.entries(pluginModules)) {
|
| 28 |
+
try {
|
| 29 |
+
const pluginName = path.split('/').pop().replace('.js', '')
|
| 30 |
+
|
| 31 |
+
console.log(`🔌 Loading plugin: ${pluginName}`)
|
| 32 |
+
|
| 33 |
+
// Plugin laden
|
| 34 |
+
const module = await importFn()
|
| 35 |
+
|
| 36 |
+
// Plugin-Kontext bereitstellen
|
| 37 |
+
const pluginContext = {
|
| 38 |
+
name: pluginName,
|
| 39 |
+
log: (msg) => console.log(`[${pluginName}]`, msg),
|
| 40 |
+
warn: (msg) => console.warn(`[${pluginName}]`, msg),
|
| 41 |
+
error: (msg) => console.error(`[${pluginName}]`, msg),
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// onPluginInit Hook aufrufen (wenn vorhanden)
|
| 45 |
+
if (module.onPluginInit) {
|
| 46 |
+
module.onPluginInit(pluginContext)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Message-Hooks registrieren
|
| 50 |
+
if (module.onMessageSent) {
|
| 51 |
+
window.addEventListener('messageSent', (e) => {
|
| 52 |
+
module.onMessageSent(pluginContext, e.detail)
|
| 53 |
+
})
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (module.onResponseReceived) {
|
| 57 |
+
window.addEventListener('responseReceived', (e) => {
|
| 58 |
+
module.onResponseReceived(pluginContext, e.detail)
|
| 59 |
+
})
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
loadedPlugins.push(pluginName)
|
| 63 |
+
console.log(`✓ Plugin ${pluginName} initialized`)
|
| 64 |
+
|
| 65 |
+
} catch (err) {
|
| 66 |
+
console.error(`✗ Failed to load plugin from ${path}:`, err)
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
setLoadedCount(loadedPlugins.length)
|
| 71 |
+
onPluginsLoad?.(loadedPlugins)
|
| 72 |
+
|
| 73 |
+
// Event für geladene Plugins
|
| 74 |
+
window.dispatchEvent(new CustomEvent('pluginsLoaded', {
|
| 75 |
+
detail: loadedPlugins
|
| 76 |
+
}))
|
| 77 |
+
|
| 78 |
+
console.log(`✓ Loaded ${loadedPlugins.length} plugin(s)`)
|
| 79 |
+
|
| 80 |
+
} catch (err) {
|
| 81 |
+
console.error('Error loading plugins:', err)
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
return null // Dieser Component rendert nichts
|
| 86 |
+
}
|
src/index.css
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--primary: #667eea;
|
| 3 |
+
--secondary: #764ba2;
|
| 4 |
+
--success: #48bb78;
|
| 5 |
+
--error: #f56565;
|
| 6 |
+
--warning: #ed8936;
|
| 7 |
+
--gray-50: #f9fafb;
|
| 8 |
+
--gray-100: #f3f4f6;
|
| 9 |
+
--gray-200: #e5e7eb;
|
| 10 |
+
--gray-300: #d1d5db;
|
| 11 |
+
--gray-400: #9ca3af;
|
| 12 |
+
--gray-500: #6b7280;
|
| 13 |
+
--gray-600: #4b5563;
|
| 14 |
+
--gray-700: #374151;
|
| 15 |
+
--gray-800: #1f2937;
|
| 16 |
+
--gray-900: #111827;
|
| 17 |
+
--border-radius: 8px;
|
| 18 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 19 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 20 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
box-sizing: border-box;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
| 31 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
| 32 |
+
min-height: 100vh;
|
| 33 |
+
color: var(--gray-900);
|
| 34 |
+
line-height: 1.6;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
html, body, #root {
|
| 38 |
+
height: 100%;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
button {
|
| 42 |
+
cursor: pointer;
|
| 43 |
+
border: none;
|
| 44 |
+
padding: 10px 16px;
|
| 45 |
+
border-radius: var(--border-radius);
|
| 46 |
+
font-size: 14px;
|
| 47 |
+
font-weight: 600;
|
| 48 |
+
transition: all 0.2s ease;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
button:hover {
|
| 52 |
+
transform: translateY(-2px);
|
| 53 |
+
box-shadow: var(--shadow-lg);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
input, textarea {
|
| 57 |
+
border: 1px solid var(--gray-300);
|
| 58 |
+
padding: 10px 12px;
|
| 59 |
+
border-radius: var(--border-radius);
|
| 60 |
+
font-size: 14px;
|
| 61 |
+
font-family: inherit;
|
| 62 |
+
transition: border-color 0.2s ease;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
input:focus, textarea:focus {
|
| 66 |
+
outline: none;
|
| 67 |
+
border-color: var(--primary);
|
| 68 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Scrollbar styling */
|
| 72 |
+
::-webkit-scrollbar {
|
| 73 |
+
width: 8px;
|
| 74 |
+
height: 8px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
::-webkit-scrollbar-track {
|
| 78 |
+
background: var(--gray-100);
|
| 79 |
+
border-radius: 10px;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
::-webkit-scrollbar-thumb {
|
| 83 |
+
background: var(--gray-300);
|
| 84 |
+
border-radius: 10px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
::-webkit-scrollbar-thumb:hover {
|
| 88 |
+
background: var(--gray-400);
|
| 89 |
+
}
|
src/main.jsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.jsx'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
// Plugin System laden
|
| 7 |
+
import { initializePlugins } from './utils/pluginManager.js'
|
| 8 |
+
|
| 9 |
+
// Plugins vor der App laden
|
| 10 |
+
initializePlugins()
|
| 11 |
+
|
| 12 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 13 |
+
<React.StrictMode>
|
| 14 |
+
<App />
|
| 15 |
+
</React.StrictMode>,
|
| 16 |
+
)
|
src/utils/api.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API Client für Kommunikation mit dem Backend
|
| 3 |
+
*
|
| 4 |
+
* Der Backend läuft auf Gradio (port 7860)
|
| 5 |
+
* Bei HF Spaces ist kein Proxy nötig, da Gradio client kommuniziert
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
// Versuche, den Client von Gradio zu laden
|
| 9 |
+
let client = null
|
| 10 |
+
|
| 11 |
+
async function initializeGradioClient() {
|
| 12 |
+
try {
|
| 13 |
+
// Bei HF Spaces: Gradio lädt seinen Client automatisch global
|
| 14 |
+
if (window.gradio_client) {
|
| 15 |
+
return window.gradio_client
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Als Fallback könnten wir den Client manuell laden
|
| 19 |
+
// aber normalerweise ist das nicht nötig
|
| 20 |
+
console.warn('Gradio client not available')
|
| 21 |
+
return null
|
| 22 |
+
} catch (err) {
|
| 23 |
+
console.error('Error initializing Gradio client:', err)
|
| 24 |
+
return null
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export async function sendMessage(prompt, systemPrompt, temperature, topP) {
|
| 29 |
+
try {
|
| 30 |
+
// Methode 1: Über Gradio API (empfohlen für HF Spaces)
|
| 31 |
+
const response = await fetch('/api/call/generate_response', {
|
| 32 |
+
method: 'POST',
|
| 33 |
+
headers: {
|
| 34 |
+
'Content-Type': 'application/json',
|
| 35 |
+
},
|
| 36 |
+
body: JSON.stringify({
|
| 37 |
+
data: [prompt, systemPrompt, temperature, topP]
|
| 38 |
+
})
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
if (!response.ok) {
|
| 42 |
+
// Fallback: Direkter HTTP Request
|
| 43 |
+
return await fallbackRequest(prompt, systemPrompt, temperature, topP)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const result = await response.json()
|
| 47 |
+
|
| 48 |
+
// Parse Gradio response
|
| 49 |
+
if (result.data && result.data[0]) {
|
| 50 |
+
const data = result.data[0]
|
| 51 |
+
return {
|
| 52 |
+
response: typeof data === 'string' ? data : data.response || 'No response',
|
| 53 |
+
tokens: data.tokens || 0,
|
| 54 |
+
time_seconds: data.time_seconds || 0,
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
return result
|
| 59 |
+
|
| 60 |
+
} catch (err) {
|
| 61 |
+
console.error('Error sending message:', err)
|
| 62 |
+
throw err
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
async function fallbackRequest(prompt, systemPrompt, temperature, topP) {
|
| 67 |
+
// Fallback für lokale Entwicklung oder andere Szenarien
|
| 68 |
+
const response = await fetch('http://localhost:7860/api/predict', {
|
| 69 |
+
method: 'POST',
|
| 70 |
+
headers: {
|
| 71 |
+
'Content-Type': 'application/json',
|
| 72 |
+
},
|
| 73 |
+
body: JSON.stringify({
|
| 74 |
+
data: [prompt, systemPrompt, temperature, topP]
|
| 75 |
+
})
|
| 76 |
+
})
|
| 77 |
+
|
| 78 |
+
if (!response.ok) {
|
| 79 |
+
throw new Error(`API request failed: ${response.statusText}`)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
const result = await response.json()
|
| 83 |
+
return result
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export async function getStats() {
|
| 87 |
+
try {
|
| 88 |
+
const response = await fetch('/api/stats')
|
| 89 |
+
if (!response.ok) {
|
| 90 |
+
throw new Error('Failed to fetch stats')
|
| 91 |
+
}
|
| 92 |
+
return await response.json()
|
| 93 |
+
} catch (err) {
|
| 94 |
+
console.error('Error fetching stats:', err)
|
| 95 |
+
return null
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
export async function loadModel(modelName) {
|
| 100 |
+
try {
|
| 101 |
+
const response = await fetch('/api/load-model', {
|
| 102 |
+
method: 'POST',
|
| 103 |
+
headers: {
|
| 104 |
+
'Content-Type': 'application/json',
|
| 105 |
+
},
|
| 106 |
+
body: JSON.stringify({ model: modelName })
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
if (!response.ok) {
|
| 110 |
+
throw new Error('Failed to load model')
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return await response.json()
|
| 114 |
+
} catch (err) {
|
| 115 |
+
console.error('Error loading model:', err)
|
| 116 |
+
throw err
|
| 117 |
+
}
|
| 118 |
+
}
|
src/utils/pluginManager.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Plugin Manager
|
| 3 |
+
* Verwaltet das automatische Laden und Initialisieren von Plugins
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
const plugins = new Map()
|
| 7 |
+
|
| 8 |
+
export function registerPlugin(name, plugin) {
|
| 9 |
+
plugins.set(name, plugin)
|
| 10 |
+
console.log(`✓ Plugin registered: ${name}`)
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function getPlugin(name) {
|
| 14 |
+
return plugins.get(name)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function getAllPlugins() {
|
| 18 |
+
return Array.from(plugins.values())
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function initializePlugins() {
|
| 22 |
+
console.log('🔌 Initializing plugin system...')
|
| 23 |
+
|
| 24 |
+
// Globale Plugin API bereitstellen
|
| 25 |
+
window.PluginAPI = {
|
| 26 |
+
register: registerPlugin,
|
| 27 |
+
get: getPlugin,
|
| 28 |
+
getAll: getAllPlugins,
|
| 29 |
+
|
| 30 |
+
// Event Emitter für Plugins
|
| 31 |
+
on: (event, callback) => {
|
| 32 |
+
window.addEventListener(event, callback)
|
| 33 |
+
},
|
| 34 |
+
|
| 35 |
+
off: (event, callback) => {
|
| 36 |
+
window.removeEventListener(event, callback)
|
| 37 |
+
},
|
| 38 |
+
|
| 39 |
+
emit: (event, data) => {
|
| 40 |
+
window.dispatchEvent(new CustomEvent(event, { detail: data }))
|
| 41 |
+
},
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
console.log('✓ Plugin API ready')
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Plugin Hook System
|
| 48 |
+
export const PluginHooks = {
|
| 49 |
+
PLUGIN_INIT: 'onPluginInit',
|
| 50 |
+
MESSAGE_SENT: 'onMessageSent',
|
| 51 |
+
RESPONSE_RECEIVED: 'onResponseReceived',
|
| 52 |
+
ERROR: 'onError',
|
| 53 |
+
}
|