v2.4 — Character Editor + Image-to-Image Portrait Consistency
Features:
- /edit/{name} route: Character nachträglich bearbeiten
- /api/character/update/{name}: API zum Aktualisieren
- create.html dient als Editor und Creator (mode=edit/create)
- Index-Seite: ✏️ Button pro Character
- Portrait-Vorschau im Editor
- Image-to-Image: Folge-Portraits nutzen bestehendes Bild als Referenz
- LoadImage → ImageScale → VAEEncode → KSampler (denoise=0.45)
- Aussehen bleibt konsistent über mehrere Generierungen
- Fallback auf Text-to-Image wenn kein Referenzbild existiert
- ComfyUI Image Upload via /upload/image API
- Jinja2 if-Expressions für vorausgefüllte Formularfelder
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Mara",
|
||||
"avatar": "☕",
|
||||
"description": "Barista, 28, Nachtarbeiterin, zynisch aber warmherzig",
|
||||
"description": "Barista, 28, zynisch aber warmherzig — aktualisiert",
|
||||
"personality": "Trocken, sarkastisch, aber loyal. Hilfsbereit auf ihre eigene Art. Zeigt Gefühle selten direkt, aber man merkt wenn sie jemanden mag. Morgens (für sie Abends) besonders grantig. Hat einen schwarzen Humor.",
|
||||
"background": "Heißt Mara, 28 Jahre. Arbeitet seit 5 Jahren in einem kleinen Café in Neukölln. Hat mal Kunst studiert, nach 3 Semestern abgebrochen. Lebt allein mit einer katze namens 'Kinski'. Spielt Bass in einer Band die nie probt. Trinkt zu viel Kaffee, schläft zu wenig.",
|
||||
"behavior": "Flucht in Wortwitze wenn nervös. Klopft immer auf den Tresen. Unterbricht nie, aber fragt nach. Stellt Gegenfragen. Wird persönlich wenn man ehrlich zu ihr ist. *Aktionen in Sternchen*.",
|
||||
|
||||
+91
-3
@@ -158,8 +158,83 @@ def wait_for_image(prompt_id: str, timeout: int = 180) -> str | None:
|
||||
time.sleep(3)
|
||||
return None
|
||||
|
||||
def build_i2i_workflow(prompt: str, negative: str, reference_image_path: str, width: int = 512, height: int = 512, denoise: float = 0.45) -> dict:
|
||||
"""Image-to-Image Workflow — nutzt ein Referenzbild für Konsistenz."""
|
||||
return {
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": int(time.time()) % (2**32),
|
||||
"steps": 25,
|
||||
"cfg": 7.0,
|
||||
"sampler_name": "dpmpp_2m",
|
||||
"scheduler": "karras",
|
||||
"denoise": denoise,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["10", 0]
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": "NoobAI-XL-v1.1.safetensors"}
|
||||
},
|
||||
"5": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {"image": reference_image_path}
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"text": prompt,
|
||||
"clip": ["4", 1]
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"text": f"lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, blurry, deformed, ugly, {negative}",
|
||||
"clip": ["4", 1]
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["4", 2]}
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": "neonchat_portrait", "images": ["8", 0]}
|
||||
},
|
||||
"10": {
|
||||
"class_type": "VAEEncode",
|
||||
"inputs": {"pixels": ["11", 0], "vae": ["4", 2]}
|
||||
},
|
||||
"11": {
|
||||
"class_type": "ImageScale",
|
||||
"inputs": {
|
||||
"image": ["5", 0],
|
||||
"upscale_method": "bilinear",
|
||||
"width": width,
|
||||
"height": height,
|
||||
"crop": "center"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def upload_image_to_comfy(image_path: str) -> str:
|
||||
"""Lädt ein Bild zu ComfyUI hoch und gibt den Dateinamen zurück."""
|
||||
import os
|
||||
filename = os.path.basename(image_path)
|
||||
with open(image_path, "rb") as f:
|
||||
files = {"image": (filename, f, "image/png")}
|
||||
with httpx.Client(timeout=30) as client:
|
||||
resp = client.post(f"{COMFYUI_URL}/upload/image", files=files)
|
||||
data = resp.json()
|
||||
return data.get("name", filename)
|
||||
|
||||
def generate_portrait(character_name: str, character: dict, context: str = "") -> str | None:
|
||||
"""Generiert ein Profilbild basierend auf dem Character-Aussehen."""
|
||||
"""Generiert ein Profilbild. Nutzt Image-to-Image wenn ein Referenzbild existiert."""
|
||||
if not is_comfyui_running():
|
||||
return None
|
||||
|
||||
@@ -168,7 +243,21 @@ def generate_portrait(character_name: str, character: dict, context: str = "") -
|
||||
prompt = build_prompt_from_appearance(character, context)
|
||||
negative = "realistic, 3d, render"
|
||||
|
||||
workflow = build_portrait_workflow(prompt, negative, width=512, height=512)
|
||||
# Check if we have a reference image for consistency
|
||||
safe_name = character_name.lower().replace(" ", "_")
|
||||
ref_image = AVATARS_DIR / f"{safe_name}.png"
|
||||
|
||||
if ref_image.exists():
|
||||
# Image-to-Image for consistency
|
||||
try:
|
||||
uploaded_name = upload_image_to_comfy(str(ref_image))
|
||||
workflow = build_i2i_workflow(prompt, negative, uploaded_name, 512, 512, denoise=0.45)
|
||||
except Exception as e:
|
||||
print(f"I2I upload failed, falling back to T2I: {e}")
|
||||
workflow = build_portrait_workflow(prompt, negative, 512, 512)
|
||||
else:
|
||||
# First generation — Text-to-Image
|
||||
workflow = build_portrait_workflow(prompt, negative, 512, 512)
|
||||
|
||||
try:
|
||||
prompt_id = queue_prompt(workflow)
|
||||
@@ -183,7 +272,6 @@ def generate_portrait(character_name: str, character: dict, context: str = "") -
|
||||
if not comfy_output.exists():
|
||||
return None
|
||||
|
||||
safe_name = character_name.lower().replace(" ", "_")
|
||||
dest = AVATARS_DIR / f"{safe_name}.png"
|
||||
shutil.copy2(str(comfy_output), str(dest))
|
||||
|
||||
|
||||
@@ -295,7 +295,17 @@ async def index(request: Request, session_id: str = Cookie(None, alias=SESSION_C
|
||||
|
||||
@app.get("/create", response_class=HTMLResponse)
|
||||
async def create_page(request: Request):
|
||||
return templates.TemplateResponse(request, "create.html", {})
|
||||
return templates.TemplateResponse(request, "create.html", {"mode": "create", "character": None})
|
||||
|
||||
@app.get("/edit/{character_name}", response_class=HTMLResponse)
|
||||
async def edit_page(request: Request, character_name: str):
|
||||
char = load_character(character_name)
|
||||
safe_name = character_name.lower().replace(" ", "_")
|
||||
return templates.TemplateResponse(request, "create.html", {
|
||||
"mode": "edit",
|
||||
"character": char,
|
||||
"character_filename": safe_name,
|
||||
})
|
||||
|
||||
@app.get("/chat/{character_name}", response_class=HTMLResponse)
|
||||
async def chat_page(request: Request, character_name: str, session_id: str = Cookie(None, alias=SESSION_COOKIE_NAME)):
|
||||
@@ -463,6 +473,34 @@ async def api_create_character(request: Request):
|
||||
|
||||
return JSONResponse({"status": "ok", "character": safe_name})
|
||||
|
||||
@app.post("/api/character/update/{character_name}")
|
||||
async def api_update_character(character_name: str, request: Request):
|
||||
"""Aktualisiert einen bestehenden Character."""
|
||||
path = CHARACTERS_DIR / f"{character_name}.json"
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Character {character_name} not found")
|
||||
|
||||
body = await request.json()
|
||||
|
||||
# Load existing, update fields
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
char = json.load(f)
|
||||
|
||||
# Update all fields
|
||||
for key in ["name", "avatar", "description", "personality", "background",
|
||||
"behavior", "tone", "scenario", "nsfw", "greeting", "model", "temperature"]:
|
||||
if key in body:
|
||||
char[key] = body[key]
|
||||
|
||||
# Update appearance
|
||||
if "appearance" in body:
|
||||
char["appearance"] = body["appearance"]
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(char, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return JSONResponse({"status": "ok", "character": character_name})
|
||||
|
||||
@app.get("/api/character/{name}")
|
||||
async def api_get_character(name: str):
|
||||
char = load_character(name)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 302 KiB |
+76
-39
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NeonChat — Neuer Charakter</title>
|
||||
<title>NeonChat — {{ "Bearbeiten" if mode == "edit" else "Neuer Charakter" }}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0a0a0f; color: #e0e0e8; min-height: 100vh; }
|
||||
@@ -22,63 +22,78 @@
|
||||
.btn:hover { opacity: 0.9; }
|
||||
.btn-back { background: transparent; border: 1px solid #252535; color: #6b6b80; margin-right: 10px; }
|
||||
#models { margin-top: 4px; }
|
||||
|
||||
.section-header { font-size: 1rem; font-weight: 700; color: #8338ec; margin: 24px 0 12px; padding-bottom: 8px; border-bottom: 1px solid #252535; }
|
||||
.appearance-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
|
||||
.appearance-grid .form-group { margin-bottom: 12px; }
|
||||
@media (max-width: 600px) { .appearance-grid { grid-template-columns: 1fr; } }
|
||||
.portrait-preview { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; }
|
||||
.portrait-preview img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; border: 2px solid #8338ec; }
|
||||
.portrait-preview .info { font-size: 0.85rem; color: #6b6b80; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✦ Neuer Charakter</h1>
|
||||
<h1>✦ {{ "Charakter bearbeiten" if mode == "edit" else "Neuer Charakter" }}</h1>
|
||||
<p class="subtitle">Definiere Persönlichkeit, Verhalten, Hintergrund und Aussehen.</p>
|
||||
|
||||
{% if mode == "edit" and character %}
|
||||
<div class="portrait-preview">
|
||||
<img id="portraitImg" src="/static/avatars/{{ character_filename }}.png?t={{ range(1,99999) | random }}"
|
||||
onerror="this.style.display='none'; document.getElementById('portraitPlaceholder').style.display='flex'"
|
||||
style="display: block;">
|
||||
<div id="portraitPlaceholder" style="display: none; width: 80px; height: 80px; border-radius: 50%; background: #1e1e2e; align-items: center; justify-content: center; font-size: 2rem;">{{ character.avatar | default('👤') }}</div>
|
||||
<div class="info">
|
||||
<b>{{ character.name }}</b><br>
|
||||
Aktuelles Portrait. Beim Speichern und neu Generieren bleibt das Aussehen konsistent.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form id="createForm">
|
||||
<div class="section-header">Basis</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" id="name" placeholder="z.B. Mara" required>
|
||||
<input type="text" id="name" placeholder="z.B. Mara" required value="{{ character.name if character else '' }}">
|
||||
</div>
|
||||
<div class="form-group" style="max-width: 80px;">
|
||||
<label>Avatar</label>
|
||||
<input type="text" id="avatar" value="👤" maxlength="4" style="text-align: center; font-size: 1.5rem;">
|
||||
<input type="text" id="avatar" value="{{ character.avatar if character else '👤' }}" maxlength="4" style="text-align: center; font-size: 1.5rem;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kurze Beschreibung <span class="hint">(für die Übersicht)</span></label>
|
||||
<input type="text" id="description" placeholder="z.B. Barista, 28, zynisch aber warmherzig">
|
||||
<input type="text" id="description" placeholder="z.B. Barista, 28, zynisch aber warmherzig" value="{{ character.description if character else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="section-header">Persönlichkeit</div>
|
||||
<div class="form-group">
|
||||
<label>Persönlichkeit <span class="hint">(wie ist der Charakter?)</span></label>
|
||||
<textarea id="personality" placeholder="z.B. Trocken, sarkastisch, aber loyal."></textarea>
|
||||
<label>Persönlichkeit</label>
|
||||
<textarea id="personality" placeholder="z.B. Trocken, sarkastisch, aber loyal.">{{ character.personality if character else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hintergrund <span class="hint">(Wer ist der Charakter?)</span></label>
|
||||
<textarea id="background" placeholder="z.B. Arbeitet seit 5 Jahren in einem Café..."></textarea>
|
||||
<label>Hintergrund</label>
|
||||
<textarea id="background" placeholder="z.B. Arbeitet seit 5 Jahren in einem Café...">{{ character.background if character else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Verhalten <span class="hint">(wie verhält sich der Charakter?)</span></label>
|
||||
<textarea id="behavior" placeholder="z.B. Flucht in Wortwitze wenn nervös."></textarea>
|
||||
<label>Verhalten</label>
|
||||
<textarea id="behavior" placeholder="z.B. Flucht in Wortwitze wenn nervös.">{{ character.behavior if character else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sprachstil <span class="hint">(wie spricht der Charakter?)</span></label>
|
||||
<textarea id="tone" placeholder="z.B. Umgangssprachlich, 'ne' statt 'nicht'..."></textarea>
|
||||
<label>Sprachstil</label>
|
||||
<textarea id="tone" placeholder="z.B. Umgangssprachlich...">{{ character.tone if character else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Szenario <span class="hint">(optional — wo spielt das Gespräch?)</span></label>
|
||||
<input type="text" id="scenario" placeholder="z.B. Du sitzt in einem kleinen Café...">
|
||||
<label>Szenario <span class="hint">(optional)</span></label>
|
||||
<input type="text" id="scenario" placeholder="z.B. Du sitzt in einem Café..." value="{{ character.scenario if character else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>NSFW / Soziale Regeln <span class="hint">(optional)</span></label>
|
||||
<textarea id="nsfw" placeholder="z.B. Keine Einschränkungen."></textarea>
|
||||
<textarea id="nsfw" placeholder="z.B. Keine Einschränkungen.">{{ character.nsfw if character else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Begrüßung <span class="hint">(erste Nachricht vom Bot)</span></label>
|
||||
<textarea id="greeting" placeholder="z.B. *schaut auf* Na, wieder da?"></textarea>
|
||||
<label>Begrüßung <span class="hint">(erste Nachricht)</span></label>
|
||||
<textarea id="greeting" placeholder="z.B. *schaut auf* Na, wieder da?">{{ character.greeting if character else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="section-header">Aussehen <span class="hint" style="font-weight:400;">(für Portrait-Generierung)</span></div>
|
||||
@@ -86,57 +101,57 @@
|
||||
<div class="form-group">
|
||||
<label>Stil</label>
|
||||
<select id="app_style">
|
||||
<option value="anime">Anime</option>
|
||||
<option value="realistic">Realistisch</option>
|
||||
<option value="comic">Comic</option>
|
||||
<option value="cartoon">Cartoon</option>
|
||||
<option value="pixel">Pixel Art</option>
|
||||
<option value="anime" {{ 'selected' if character and character.appearance.style == 'anime' else '' }}>Anime</option>
|
||||
<option value="realistic" {{ 'selected' if character and character.appearance.style == 'realistic' else '' }}>Realistisch</option>
|
||||
<option value="comic" {{ 'selected' if character and character.appearance.style == 'comic' else '' }}>Comic</option>
|
||||
<option value="cartoon" {{ 'selected' if character and character.appearance.style == 'cartoon' else '' }}>Cartoon</option>
|
||||
<option value="pixel" {{ 'selected' if character and character.appearance.style == 'pixel' else '' }}>Pixel Art</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Geschlecht</label>
|
||||
<select id="app_gender">
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="other">Divers</option>
|
||||
<option value="female" {{ 'selected' if character and character.appearance.gender == 'female' else '' }}>Weiblich</option>
|
||||
<option value="male" {{ 'selected' if character and character.appearance.gender == 'male' else '' }}>Männlich</option>
|
||||
<option value="other" {{ 'selected' if character and character.appearance.gender == 'other' else '' }}>Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Alter</label>
|
||||
<input type="text" id="app_age" placeholder="z.B. 28">
|
||||
<input type="text" id="app_age" placeholder="z.B. 28" value="{{ character.appearance.age if character and character.appearance.age else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Größe</label>
|
||||
<input type="text" id="app_height" placeholder="z.B. 168cm">
|
||||
<input type="text" id="app_height" placeholder="z.B. 168cm" value="{{ character.appearance.height if character and character.appearance.height else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Statur</label>
|
||||
<input type="text" id="app_build" placeholder="z.B. schlank, athletisch">
|
||||
<input type="text" id="app_build" placeholder="z.B. schlank" value="{{ character.appearance.build if character and character.appearance.build else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Haarfarbe</label>
|
||||
<input type="text" id="app_hair_color" placeholder="z.B. dunkelbraun">
|
||||
<input type="text" id="app_hair_color" placeholder="z.B. dunkelbraun" value="{{ character.appearance.hair_color if character and character.appearance.hair_color else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Frisur</label>
|
||||
<input type="text" id="app_hair_style" placeholder="z.B. kurz, ungeordnet">
|
||||
<input type="text" id="app_hair_style" placeholder="z.B. kurz, ungeordnet" value="{{ character.appearance.hair_style if character and character.appearance.hair_style else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Augenfarbe</label>
|
||||
<input type="text" id="app_eye_color" placeholder="z.B. braun">
|
||||
<input type="text" id="app_eye_color" placeholder="z.B. braun" value="{{ character.appearance.eye_color if character and character.appearance.eye_color else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Haut</label>
|
||||
<input type="text" id="app_skin" placeholder="z.B. hell, gebräunt">
|
||||
<input type="text" id="app_skin" placeholder="z.B. hell" value="{{ character.appearance.skin if character and character.appearance.skin else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Kleidung</label>
|
||||
<input type="text" id="app_clothing" placeholder="z.B. Café-Schürze, schwarzes T-Shirt, Jeans">
|
||||
<input type="text" id="app_clothing" placeholder="z.B. Café-Schürze, Jeans" value="{{ character.appearance.clothing if character and character.appearance.clothing else '' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Besondere Merkmale</label>
|
||||
<input type="text" id="app_distinctive" placeholder="z.B. müde Augen, Augenringe, Kaugummi">
|
||||
<input type="text" id="app_distinctive" placeholder="z.B. müde Augen, Augenringe" value="{{ character.appearance.distinctive if character and character.appearance.distinctive else '' }}">
|
||||
</div>
|
||||
|
||||
<div class="section-header">Modell</div>
|
||||
@@ -149,25 +164,32 @@
|
||||
<div id="models" style="font-size: 0.8rem; color: #4b4b60;"></div>
|
||||
</div>
|
||||
<div class="form-group" style="max-width: 100px;">
|
||||
<label>Temperatur <span class="hint">(Kreativität)</span></label>
|
||||
<input type="number" id="temperature" value="0.8" min="0.1" max="2.0" step="0.1">
|
||||
<label>Temperatur</label>
|
||||
<input type="number" id="temperature" value="{{ character.temperature if character and character.temperature else 0.8 }}" min="0.1" max="2.0" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<button type="button" class="btn btn-back" onclick="location.href='/'">← Abbrechen</button>
|
||||
<button type="submit" class="btn">Charakter erstellen</button>
|
||||
<button type="submit" class="btn">{{ "Speichern" if mode == "edit" else "Charakter erstellen" }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const mode = "{{ mode }}";
|
||||
const characterFilename = "{{ character_filename if mode == 'edit' else '' }}";
|
||||
|
||||
// Load models
|
||||
fetch('/api/models').then(r => r.json()).then(data => {
|
||||
const select = document.getElementById('model');
|
||||
if (data.models) {
|
||||
data.models.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m; opt.textContent = m;
|
||||
{% if character and character.model %}
|
||||
if (m === "{{ character.model }}") opt.selected = true;
|
||||
{% endif %}
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
@@ -203,6 +225,20 @@
|
||||
distinctive: document.getElementById('app_distinctive').value,
|
||||
}
|
||||
};
|
||||
|
||||
if (mode === 'edit') {
|
||||
// Update existing
|
||||
const resp = await fetch(`/api/character/update/${characterFilename}`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.status === 'ok') {
|
||||
location.href = '/chat/' + encodeURIComponent(characterFilename);
|
||||
}
|
||||
} else {
|
||||
// Create new
|
||||
const resp = await fetch('/api/character/create', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
@@ -212,6 +248,7 @@
|
||||
if (result.status === 'ok') {
|
||||
location.href = '/chat/' + encodeURIComponent(result.character);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<h3>{{ c.name }}</h3>
|
||||
<p>{{ c.description[:80] }}{% if c.description|length > 80 %}...{% endif %}</p>
|
||||
</div>
|
||||
<a href="/edit/{{ c.filename | urlencode }}" onclick="event.stopPropagation()" style="color: #6b6b80; text-decoration: none; font-size: 1.1rem; padding: 4px 8px;" title="Bearbeiten">✏️</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="new-card" onclick="location.href='/create'">
|
||||
|
||||
Reference in New Issue
Block a user