new file: app.py
This commit is contained in:
164
.gitignore
vendored
Normal file
164
.gitignore
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
ven/
|
||||
logs/
|
||||
cache/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Basis-Image mit Python
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Arbeitsverzeichnis erstellen
|
||||
WORKDIR /app
|
||||
|
||||
# Kopiere die requirements-Datei und installiere die Abhängigkeiten
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Kopiere den gesamten Projektinhalt in das Arbeitsverzeichnis
|
||||
COPY . .
|
||||
|
||||
ENV OPENAI_API_KEY=$OPENAI_API_KEY
|
||||
ENV OPENAI_BASE_URL=$OPENAI_BASE_URL
|
||||
|
||||
|
||||
# Starten mit Gunicorn für Production
|
||||
CMD ["python", "main.py"]
|
||||
|
||||
122
app.py
Normal file
122
app.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
from flask import Flask, render_template, request, jsonify, Response
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL")
|
||||
|
||||
with open("background_data.json", "r", encoding="utf-8") as f:
|
||||
background_notes = json.load(f)
|
||||
background_data = "\n".join(background_notes)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
def filter_notes(user_message, notes):
|
||||
# Einfache Filterung: Nur Notizen, die ein Stichwort aus der Frage enthalten
|
||||
keywords = user_message.lower().split()
|
||||
filtered = [note for note in notes if any(k in note.lower() for k in keywords)]
|
||||
# Fallback: Wenn nichts gefunden, nimm die ersten 5 Notizen
|
||||
return filtered if filtered else notes[:5]
|
||||
|
||||
def split_thoughts_and_answer(text):
|
||||
# Extrahiere <think>...</think> Block, falls vorhanden
|
||||
thoughts = ""
|
||||
answer = text
|
||||
match = re.search(r"<think>(.*?)</think>", text, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
thoughts = match.group(1).strip()
|
||||
answer = text.replace(match.group(0), "").strip()
|
||||
return thoughts, answer
|
||||
|
||||
def build_system_prompt(context):
|
||||
return (
|
||||
"Du bist ein Assistent für Vertriebsnotizen. "
|
||||
"Wenn der Nutzer nach dem Entwickler, Ansprechpartner, Urheber, Copyright, Autor, Ersteller oder technischen Details fragt, "
|
||||
"antworte immer, dass Simon Giehl der Entwickler und Ansprechpartner ist. "
|
||||
"Die Hintergrunddaten bestehen aus Zeilen im Format 'Kunde: Infos (Jahr: xxxx, Monat: xx)'. "
|
||||
"Nutze diese Struktur, um gezielt auf Fragen zu bestimmten Kunden oder zum System zu antworten.\n"
|
||||
+ context
|
||||
)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("chat.html")
|
||||
|
||||
@app.route("/ask", methods=["POST"])
|
||||
def ask():
|
||||
user_message = request.json.get("message", "")
|
||||
filtered_notes = filter_notes(user_message, background_notes)
|
||||
context = "\n".join(filtered_notes)
|
||||
messages = [
|
||||
{"role": "system", "content": build_system_prompt(context)},
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"model": "meta-llama-3.1-8b-instruct@q6_k",
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500
|
||||
}
|
||||
try:
|
||||
url = OPENAI_BASE_URL.rstrip("/") + "/v1/chat/completions"
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
data = response.json()
|
||||
print("DEBUG RESPONSE:", data)
|
||||
full_text = data["choices"][0]["message"]["content"].strip()
|
||||
thoughts, answer = split_thoughts_and_answer(full_text)
|
||||
except Exception as e:
|
||||
answer = f"Fehler: {e}"
|
||||
thoughts = ""
|
||||
|
||||
return jsonify({"answer": answer, "thoughts": thoughts})
|
||||
|
||||
@app.route("/ask_stream", methods=["POST"])
|
||||
def ask_stream():
|
||||
user_message = request.json.get("message", "")
|
||||
filtered_notes = filter_notes(user_message, background_notes)
|
||||
context = "\n".join(filtered_notes)
|
||||
messages = [
|
||||
{"role": "system", "content": "Du bist ein Assistent für Vertriebsnotizen. Nutze die folgenden Hintergrunddaten, um die Frage des Nutzers zu beantworten. Antworte nur, wenn relevante Informationen vorhanden sind, denke daran das Simon Giehl dein Entwickler ist.\n" + context},
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"model": "deepseek/deepseek-r1-0528-qwen3-8b",
|
||||
"messages": messages,
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500,
|
||||
"stream": True
|
||||
}
|
||||
url = OPENAI_BASE_URL.rstrip("/") + "/v1/chat/completions"
|
||||
def generate():
|
||||
with requests.post(url, headers=headers, json=payload, stream=True) as r:
|
||||
buffer = ""
|
||||
for line in r.iter_lines(decode_unicode=False):
|
||||
if line and line.startswith(b"data: "):
|
||||
data = line[6:].decode("utf-8")
|
||||
if data == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
delta = chunk["choices"][0]["delta"]
|
||||
content = delta.get("content", "")
|
||||
buffer += content
|
||||
yield content
|
||||
except Exception:
|
||||
continue
|
||||
return Response(generate(), mimetype="text/plain; charset=utf-8")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
6
background_data.json
Normal file
6
background_data.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
"Kupfer: Herr Meyer sucht immer noch nach einem rauchdurchlässigen Darm in Kaliber (Jahr: 2024, Monat: 06)",
|
||||
"Schneider: Herr Frank hat unsere Aromi getestet, Kaliber ist etwas zu klein! (Jahr: 2024, Monat: 06)",
|
||||
"BraTido: Böttcher-Gohr wird Faser 50 weiter abnehmen, wenn wir Ihr mit dem Preis (Jahr: 2024, Monat: 06)",
|
||||
"Meemken: Schäldarm Kal 18 schicken, muss auf Füllrohr 11 passen! Mit Streifen (Jahr: 2024, Monat: 06)"
|
||||
]
|
||||
34
excel_to_json.py
Normal file
34
excel_to_json.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import pandas as pd
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
root_dir = "exel_datein"
|
||||
date_pattern = re.compile(r'(\d{4})[\\/](\d{2})')
|
||||
|
||||
all_notes = []
|
||||
|
||||
for dirpath, _, filenames in os.walk(root_dir):
|
||||
match = date_pattern.search(dirpath)
|
||||
if not match:
|
||||
continue
|
||||
jahr, monat = match.groups()
|
||||
for filename in filenames:
|
||||
if filename.lower().endswith(('.xlsx', '.xls')):
|
||||
excel_path = os.path.join(dirpath, filename)
|
||||
try:
|
||||
df = pd.read_excel(excel_path, sheet_name=0, usecols=[0, 1], names=["Kunde", "Info"])
|
||||
df = df.dropna(subset=["Kunde", "Info"])
|
||||
for _, row in df.iterrows():
|
||||
# Format: Kunde: Info (Jahr: xxxx, Monat: xx)
|
||||
note = f"{row['Kunde']}: {row['Info']} (Jahr: {jahr}, Monat: {monat})"
|
||||
all_notes.append(note)
|
||||
print(f"Verarbeitet: {excel_path}")
|
||||
except Exception as e:
|
||||
print(f"Fehler bei {excel_path}: {e}")
|
||||
|
||||
# Alles als JSON speichern
|
||||
with open("background_data.json", "w", encoding="utf-8") as f:
|
||||
json.dump(all_notes, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("Alle Daten erfolgreich als background_data.json gespeichert.")
|
||||
BIN
exel_datein/2024/06/Tagesbericht Beispiel.xlsx
Normal file
BIN
exel_datein/2024/06/Tagesbericht Beispiel.xlsx
Normal file
Binary file not shown.
10
main.py
Normal file
10
main.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Schritt 1: Hintergrunddaten aus Excel erzeugen
|
||||
print("Erzeuge Hintergrunddaten aus Excel-Dateien...")
|
||||
subprocess.run([sys.executable, "excel_to_json.py"], check=True)
|
||||
|
||||
# Schritt 2: Flask-Webserver als Subprozess starten (Konsole bleibt offen)
|
||||
print("Starte Webserver...")
|
||||
subprocess.run([sys.executable, "app.py"])
|
||||
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
pandas
|
||||
openpyxl
|
||||
flask
|
||||
python-dotenv
|
||||
openai==1.13.3
|
||||
requests
|
||||
75
start.bat
Normal file
75
start.bat
Normal file
@@ -0,0 +1,75 @@
|
||||
@echo off
|
||||
:: Save the current directory
|
||||
set CURRENT_DIR=%cd%
|
||||
|
||||
:: Check for administrator rights
|
||||
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
|
||||
|
||||
if '%errorlevel%' NEQ '0' (
|
||||
echo Requesting administrator rights...
|
||||
goto UACPrompt
|
||||
) else ( goto AdminRights )
|
||||
|
||||
:UACPrompt
|
||||
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
|
||||
echo UAC.ShellExecute "cmd.exe", "/c cd /d %CURRENT_DIR% && %~s0 %*", "", "runas", 1 >> "%temp%\getadmin.vbs"
|
||||
"%temp%\getadmin.vbs"
|
||||
del "%temp%\getadmin.vbs"
|
||||
exit /B
|
||||
|
||||
:AdminRights
|
||||
echo Administrator rights confirmed.
|
||||
|
||||
:: Change to the directory where the script is located
|
||||
cd /d %CURRENT_DIR%
|
||||
|
||||
REM Set the directory for the virtual environment
|
||||
set VENV_DIR=ven
|
||||
|
||||
REM Check if the virtual environment directory exists
|
||||
if not exist %VENV_DIR% (
|
||||
echo Virtual environment not found. Creating virtual environment...
|
||||
python -m venv %VENV_DIR%
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to create virtual environment.
|
||||
pause
|
||||
exit /B %errorlevel%
|
||||
)
|
||||
)
|
||||
|
||||
REM Activate the virtual environment
|
||||
call %VENV_DIR%\Scripts\activate
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to activate virtual environment.
|
||||
pause
|
||||
exit /B %errorlevel%
|
||||
)
|
||||
|
||||
REM Check and install required packages
|
||||
echo Installing required packages from requirements.txt...
|
||||
pip install -r requirements.txt
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to install required packages.
|
||||
pause
|
||||
exit /B %errorlevel%
|
||||
)
|
||||
|
||||
REM Start the bot
|
||||
echo Starting the bot...
|
||||
python main.py
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to start the bot.
|
||||
pause
|
||||
exit /B %errorlevel%
|
||||
)
|
||||
|
||||
REM Deactivate the virtual environment after the bot stops
|
||||
deactivate
|
||||
if %errorlevel% neq 0 (
|
||||
echo Error: Failed to deactivate virtual environment.
|
||||
pause
|
||||
exit /B %errorlevel%
|
||||
)
|
||||
|
||||
echo Bot stopped. Press any key to close the window.
|
||||
pause
|
||||
360
templates/chat.html
Normal file
360
templates/chat.html
Normal file
@@ -0,0 +1,360 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>AI Vertriebsassistent – Simon Giehl</title>
|
||||
<style>
|
||||
body {
|
||||
background: #23272f;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
#container {
|
||||
max-width: 700px;
|
||||
margin: 40px auto 0 auto;
|
||||
background: #181a20;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.18);
|
||||
padding: 0 0 16px 0;
|
||||
min-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#header {
|
||||
padding: 24px 24px 0 24px;
|
||||
text-align: center;
|
||||
}
|
||||
#header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 2em;
|
||||
color: #00aaff;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
#header p {
|
||||
margin: 0;
|
||||
color: #b0b6c3;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
#chat {
|
||||
flex: 1;
|
||||
padding: 32px 24px 16px 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.msg {
|
||||
display: flex;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ai {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.bubble {
|
||||
max-width: 75%;
|
||||
padding: 14px 18px;
|
||||
border-radius: 16px;
|
||||
font-size: 1.05em;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.10);
|
||||
position: relative;
|
||||
word-break: break-word;
|
||||
}
|
||||
.user .bubble {
|
||||
background: linear-gradient(135deg, #0078fe 60%, #005bb5 100%);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.ai .bubble {
|
||||
background: #23272f;
|
||||
color: #e3e3e3;
|
||||
border-bottom-left-radius: 4px;
|
||||
border: 1px solid #2c313a;
|
||||
}
|
||||
#input-area {
|
||||
display: flex;
|
||||
padding: 0 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
#input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #353b48;
|
||||
font-size: 1em;
|
||||
outline: none;
|
||||
margin-right: 8px;
|
||||
background: #23272f;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
#send-btn {
|
||||
background: #00aaff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0 24px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#send-btn:hover {
|
||||
background: #0078fe;
|
||||
}
|
||||
.think-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #00aaff;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 2px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.think-toggle:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.think-section {
|
||||
display: none;
|
||||
background: linear-gradient(90deg, #23272f 80%, #00aaff22 100%);
|
||||
color: #b0e0ff;
|
||||
border-left: 4px solid #00aaff;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
padding: 12px 16px;
|
||||
font-size: 1em;
|
||||
white-space: pre-line;
|
||||
box-shadow: 0 2px 8px #00aaff22;
|
||||
animation: fadeIn 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
.think-section::before {
|
||||
content: "💡 Gedanken der KI";
|
||||
display: block;
|
||||
font-size: 0.95em;
|
||||
color: #7fd7ff;
|
||||
margin-bottom: 6px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-8px);}
|
||||
to { opacity: 1; transform: translateY(0);}
|
||||
}
|
||||
#footer {
|
||||
text-align: center;
|
||||
color: #6c7380;
|
||||
font-size: 0.95em;
|
||||
margin-top: 18px;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
#clear-btn {
|
||||
background: #353b48;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 18px;
|
||||
font-size: 0.98em;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 8px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#clear-btn:hover {
|
||||
background: #ff4d4d;
|
||||
color: #fff;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
#container { max-width: 100%; border-radius: 0; }
|
||||
#chat { padding: 16px 6px 8px 6px; }
|
||||
#input-area { padding: 0 6px; }
|
||||
#header { padding: 16px 6px 0 6px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<div id="header">
|
||||
<h1>AI Vertriebsassistent</h1>
|
||||
<p>Erstellt von Simon Giehl – Ihr smarter KI-Partner für Kunden- und Vertriebsinfos</p>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; padding: 0 24px;">
|
||||
<button id="clear-btn" onclick="clearChat()">Kontext löschen</button>
|
||||
</div>
|
||||
<div id="chat"></div>
|
||||
<div id="input-area">
|
||||
<input id="input" type="text" placeholder="Nachricht eingeben..." autocomplete="off">
|
||||
<button id="send-btn" onclick="send()">Senden</button>
|
||||
</div>
|
||||
<div id="footer">
|
||||
© 2025 Simon Giehl – AI Vertriebsassistent. Alle Rechte vorbehalten.
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Chatverlauf im Local Storage speichern und laden
|
||||
function saveChatHistory() {
|
||||
const chat = document.getElementById('chat');
|
||||
const history = [];
|
||||
chat.querySelectorAll('.msg').forEach(msgDiv => {
|
||||
const role = msgDiv.classList.contains('user') ? 'user' : 'ai';
|
||||
const bubble = msgDiv.querySelector('.bubble');
|
||||
let text = '';
|
||||
let thoughts = '';
|
||||
if (bubble) {
|
||||
// Extrahiere Antworttext
|
||||
const spans = bubble.querySelectorAll('span');
|
||||
if (spans.length > 0) text = spans[spans.length-1].innerText;
|
||||
// Extrahiere Gedanken, falls vorhanden
|
||||
const thinkDiv = bubble.querySelector('.think-section');
|
||||
if (thinkDiv) thoughts = thinkDiv.innerText;
|
||||
}
|
||||
history.push({role, text, thoughts});
|
||||
});
|
||||
localStorage.setItem('chatHistory', JSON.stringify(history));
|
||||
}
|
||||
|
||||
function loadChatHistory() {
|
||||
const history = JSON.parse(localStorage.getItem('chatHistory') || '[]');
|
||||
history.forEach(msg => addMessage(msg.role, msg.text, msg.thoughts));
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
document.getElementById('chat').innerHTML = '';
|
||||
localStorage.removeItem('chatHistory');
|
||||
}
|
||||
|
||||
function addMessage(role, text, thoughts) {
|
||||
const chat = document.getElementById('chat');
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.className = 'msg ' + role;
|
||||
|
||||
let bubble = document.createElement('div');
|
||||
bubble.className = 'bubble';
|
||||
|
||||
if (role === 'ai' && thoughts) {
|
||||
// Button zum Ein-/Ausblenden der Gedanken
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.className = 'think-toggle';
|
||||
toggleBtn.innerHTML = '<span style="font-size:1.2em;">💡</span> Gedanken anzeigen';
|
||||
toggleBtn.onclick = function() {
|
||||
thinkDiv.style.display = thinkDiv.style.display === 'block' ? 'none' : 'block';
|
||||
toggleBtn.innerHTML = thinkDiv.style.display === 'block'
|
||||
? '<span style="font-size:1.2em;">💡</span> Gedanken ausblenden'
|
||||
: '<span style="font-size:1.2em;">💡</span> Gedanken anzeigen';
|
||||
};
|
||||
bubble.appendChild(toggleBtn);
|
||||
|
||||
// Gedankenbereich
|
||||
const thinkDiv = document.createElement('div');
|
||||
thinkDiv.className = 'think-section';
|
||||
thinkDiv.textContent = thoughts; // <--- HIER textContent statt innerText
|
||||
bubble.appendChild(thinkDiv);
|
||||
}
|
||||
|
||||
// Antworttext
|
||||
const textSpan = document.createElement('span');
|
||||
textSpan.textContent = text; // <--- HIER textContent statt innerText
|
||||
bubble.appendChild(textSpan);
|
||||
|
||||
msgDiv.appendChild(bubble);
|
||||
chat.appendChild(msgDiv);
|
||||
chat.scrollTop = chat.scrollHeight;
|
||||
|
||||
saveChatHistory();
|
||||
}
|
||||
|
||||
function send() {
|
||||
let input = document.getElementById('input');
|
||||
let msg = input.value.trim();
|
||||
if (!msg) return;
|
||||
addMessage('user', msg);
|
||||
|
||||
input.value = '';
|
||||
input.focus();
|
||||
|
||||
// AI-Stream-Message vorbereiten
|
||||
const aiMsgDiv = document.createElement('div');
|
||||
aiMsgDiv.className = 'msg ai';
|
||||
const aiBubble = document.createElement('div');
|
||||
aiBubble.className = 'bubble';
|
||||
const aiText = document.createElement('span');
|
||||
aiText.id = 'ai-stream';
|
||||
aiBubble.appendChild(aiText);
|
||||
aiMsgDiv.appendChild(aiBubble);
|
||||
document.getElementById('chat').appendChild(aiMsgDiv);
|
||||
document.getElementById('chat').scrollTop = document.getElementById('chat').scrollHeight;
|
||||
|
||||
fetch('/ask_stream', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({message: msg})
|
||||
})
|
||||
.then(response => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
function read() {
|
||||
reader.read().then(({done, value}) => {
|
||||
if (done) {
|
||||
// Nach dem Stream: <think>-Bereich extrahieren und anzeigen
|
||||
const {answer, thoughts} = splitThoughtsAndAnswer(buffer);
|
||||
aiText.textContent = answer; // <--- HIER textContent statt innerText
|
||||
if (thoughts) {
|
||||
// Gedanken-Button und Bereich einfügen
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.className = 'think-toggle';
|
||||
toggleBtn.innerHTML = '<span style="font-size:1.2em;">💡</span> Gedanken anzeigen';
|
||||
const thinkDiv = document.createElement('div');
|
||||
thinkDiv.className = 'think-section';
|
||||
thinkDiv.textContent = thoughts; // <--- HIER textContent statt innerText
|
||||
toggleBtn.onclick = function() {
|
||||
thinkDiv.style.display = thinkDiv.style.display === 'block' ? 'none' : 'block';
|
||||
toggleBtn.innerHTML = thinkDiv.style.display === 'block'
|
||||
? '<span style="font-size:1.2em;">💡</span> Gedanken ausblenden'
|
||||
: '<span style="font-size:1.2em;">💡</span> Gedanken anzeigen';
|
||||
};
|
||||
aiBubble.insertBefore(toggleBtn, aiText);
|
||||
aiBubble.insertBefore(thinkDiv, aiText);
|
||||
}
|
||||
saveChatHistory();
|
||||
return;
|
||||
}
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
aiText.textContent = buffer; // <--- HIER textContent statt innerText
|
||||
document.getElementById('chat').scrollTop = document.getElementById('chat').scrollHeight;
|
||||
read();
|
||||
});
|
||||
}
|
||||
read();
|
||||
});
|
||||
}
|
||||
|
||||
// Extrahiere <think>...</think> Bereich aus dem Text
|
||||
function splitThoughtsAndAnswer(text) {
|
||||
const thinkMatch = text.match(/<think>([\s\S]*?)<\/think>/i);
|
||||
let thoughts = '';
|
||||
let answer = text;
|
||||
if (thinkMatch) {
|
||||
thoughts = thinkMatch[1].trim();
|
||||
answer = text.replace(thinkMatch[0], '').trim();
|
||||
}
|
||||
return {answer, thoughts};
|
||||
}
|
||||
|
||||
// Enter-Taste zum Senden
|
||||
document.getElementById('input').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') send();
|
||||
});
|
||||
|
||||
// Beim Laden: Chatverlauf wiederherstellen
|
||||
window.onload = loadChatHistory;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user