From 77a1874c0b70fb265a7ed3bd0db762673b535c6a Mon Sep 17 00:00:00 2001
From: daviirodrig <30713947+daviirodrig@users.noreply.github.com>
Date: Thu, 22 Feb 2024 02:41:09 -0300
Subject: [PATCH] separate dashboard code in tabs
---
backend/src/components/tabs/config.py | 187 +++++++++++
backend/src/components/tabs/context.py | 28 ++
backend/src/components/tabs/customtts.py | 95 ++++++
backend/src/components/tabs/elevenlabs.py | 71 ++++
backend/src/components/tabs/logs.py | 15 +
backend/src/dashboard.py | 385 +---------------------
backend/src/main.py | 3 +-
7 files changed, 409 insertions(+), 375 deletions(-)
create mode 100644 backend/src/components/tabs/config.py
create mode 100644 backend/src/components/tabs/context.py
create mode 100644 backend/src/components/tabs/customtts.py
create mode 100644 backend/src/components/tabs/elevenlabs.py
create mode 100644 backend/src/components/tabs/logs.py
diff --git a/backend/src/components/tabs/config.py b/backend/src/components/tabs/config.py
new file mode 100644
index 0000000..ecb8619
--- /dev/null
+++ b/backend/src/components/tabs/config.py
@@ -0,0 +1,187 @@
+import gradio as gr
+from src.config import global_config
+from src.prompts import prompt_manager
+from src.websocket import ws
+from loguru import logger
+
+
+def change_prompt(prompt_id: str, voice_id: str, model: str, clear_context: bool):
+ logger.info(f"Setting prompt to {prompt_id} and voice {voice_id}")
+ if prompt_id not in list(prompt_manager.prompts):
+ return f"Prompt {prompt_id} does not exist"
+ global_config.elevenlabs_voice_id = voice_id
+ global_config.elevenlabs_model = model
+ prompt_manager.set_current_prompt(prompt_id, clear_context)
+ r = f"Prompt setted to {prompt_id} with model {model} and voice {voice_id}"
+ if clear_context:
+ r += " and context cleared"
+ return r
+
+
+def save_prompt(prompt_id: str, prompt: str):
+ logger.info(f"Saving prompt {prompt_id}")
+ prompt_manager.new_custom_prompt(prompt_id, prompt)
+
+
+async def change_personality(personality_id: str, checkboxes: list):
+ clear_context = "Clear context" in checkboxes
+ notify_minecraft = "Notify Minecraft" in checkboxes
+ logger.info(f"Setting personality to {personality_id}")
+
+ if personality_id not in list(prompt_manager.personalities):
+ return f"Personality {personality_id} does not exist"
+
+ prompt_manager.set_personality(personality_id, clear_context)
+ prompt_manager.set_current_prompt(prompt_manager.personalities[personality_id]["prompt_id"], clear_context)
+ global_config.elevenlabs_voice_id = prompt_manager.personalities[personality_id]["voice_id"]
+ global_config.elevenlabs_model = prompt_manager.personalities[personality_id]["model"]
+
+ if notify_minecraft:
+ await ws.broadcast({"action": "new_personality", "data": personality_id})
+
+ return f"Personality setted to {personality_id}"
+
+
+def new_personality(personality_id: str, prompt_id: str, model: str, voice_id: str):
+ logger.info(f"Creating new personality {personality_id}")
+
+ prompt_manager.personalities[personality_id] = {
+ "prompt_id": prompt_id,
+ "voice_id": voice_id,
+ "model": model,
+ }
+ prompt_manager.save()
+ return f"Personality {personality_id} created"
+
+
+def config_tab():
+ with gr.Tab("Config") as tab:
+ with gr.Tab("Global Config"):
+ gr.Markdown(
+ value=lambda: global_config.as_markdown(),
+ every=5,
+ )
+
+ with gr.Tab("Change Personality"):
+ gr.Interface(
+ fn=change_personality,
+ inputs=[
+ gr.Textbox(
+ label="Personality",
+ value=lambda: prompt_manager.current_personality_id,
+ ),
+ gr.CheckboxGroup(
+ label="Clear context",
+ choices=["Clear context", "Notify Minecraft"],
+ container=False,
+ ),
+ ],
+ outputs="text",
+ allow_flagging="never",
+ )
+
+ with gr.Tab("Personalities"):
+
+ def personalities_html():
+ return "\n".join(
+ [
+ f"\n{key}
\n\n{prompt_manager.personalities[key]}\n
\n "
+ for key in prompt_manager.personalities
+ ]
+ )
+
+ gr.HTML(
+ value=lambda: personalities_html(),
+ every=5,
+ )
+
+ with gr.Tab("New personality"):
+ gr.Interface(
+ fn=new_personality,
+ inputs=[
+ gr.Textbox(
+ label="Personality name",
+ placeholder="personality_name",
+ ),
+ gr.Textbox(
+ label="Prompt ID",
+ placeholder="prompt0",
+ ),
+ gr.Dropdown(
+ label="Model ID",
+ choices=["eleven_multilingual_v2", "eleven_multilingual_v1"],
+ ),
+ gr.Textbox(
+ label="Voice ID",
+ placeholder="9Fa9ozDyMkNFPnyRbRZD",
+ ),
+ ],
+ outputs="text",
+ allow_flagging="never",
+ )
+
+ with gr.Tab("Change Prompt"):
+ gr.Interface(
+ fn=change_prompt,
+ inputs=[
+ gr.Textbox(
+ label="Prompt ID",
+ value=lambda: prompt_manager.current_prompt_id,
+ ),
+ gr.Textbox(
+ label="Voice ID",
+ value=lambda: global_config.elevenlabs_voice_id,
+ ),
+ gr.Dropdown(
+ label="Voice Model",
+ choices=["eleven_multilingual_v2", "eleven_multilingual_v1"],
+ value=lambda: global_config.elevenlabs_model,
+ ),
+ gr.Checkbox(label="Clear context", value=False),
+ ],
+ outputs="text",
+ allow_flagging="never",
+ )
+ gr.Textbox(
+ value=lambda: f"Current prompt: {prompt_manager.current_prompt_id}\nCurrent voice: {global_config.elevenlabs_voice_id}\nCurrent model: {global_config.elevenlabs_model}",
+ interactive=False,
+ every=1,
+ lines=3,
+ max_lines=3,
+ container=False,
+ )
+
+ with gr.Tab("Prompts"):
+
+ def prompts_html():
+ return "\n".join(
+ [
+ f"\n{id}
\n\n{prompt}\n
\n "
+ for id, prompt in prompt_manager.prompts.items()
+ ]
+ )
+
+ gr.HTML(lambda: prompts_html(), every=5)
+
+ with gr.Tab(label="New Prompt"):
+ custom_prompt_id = gr.Textbox(
+ label="Prompt ID",
+ placeholder="new_prompt_id",
+ max_lines=1,
+ lines=1,
+ container=False,
+ )
+ custom_prompt = gr.Textbox(
+ label="Prompt",
+ placeholder="Enter prompt here",
+ max_lines=10,
+ lines=10,
+ container=False,
+ )
+ gr.Button(
+ "Save Prompt",
+ ).click(
+ save_prompt,
+ inputs=[custom_prompt_id, custom_prompt],
+ )
+ return tab
diff --git a/backend/src/components/tabs/context.py b/backend/src/components/tabs/context.py
new file mode 100644
index 0000000..87318f2
--- /dev/null
+++ b/backend/src/components/tabs/context.py
@@ -0,0 +1,28 @@
+import gradio as gr
+from src.context import context
+
+def get_context_as_chatbot() -> list[tuple[str, str]]:
+ full_messages = []
+ question_response = []
+ for entries in context.all():
+ question_response.append(entries["content"])
+ if len(question_response) >= 2:
+ full_messages.append(question_response)
+ question_response = []
+ if len(context.all()) % 2 != 0:
+ # Make sure all messages are pairs
+ full_messages.append(question_response)
+ full_messages[-1].append(" ")
+ return full_messages
+
+def context_tab():
+ with gr.Tab("Context") as tab:
+ gr.Chatbot(
+ value=get_context_as_chatbot,
+ every=1,
+ container=False,
+ height=700,
+ layout="panel",
+ show_copy_button=True,
+ )
+ return tab
diff --git a/backend/src/components/tabs/customtts.py b/backend/src/components/tabs/customtts.py
new file mode 100644
index 0000000..0149753
--- /dev/null
+++ b/backend/src/components/tabs/customtts.py
@@ -0,0 +1,95 @@
+import gradio as gr
+from loguru import logger
+from src.chatgpt import chat
+from src.context import context
+from src.tts import tts
+from src.handler import event_handler
+
+
+def customTTS_tab(loop):
+ with gr.Tab("Custom TTS") as tab:
+ with gr.Row():
+ with gr.Column(scale=2):
+ with gr.Group():
+ gpt_input = gr.Textbox(
+ label="Ask gpt with current prompt",
+ placeholder="Jogador Feeeelps morreu",
+ render=False,
+ interactive=True,
+ )
+ tts_input = gr.Textbox(
+ label="GPT Output",
+ placeholder="Ah, que pena felps morreu.",
+ render=False,
+ interactive=True,
+ )
+
+ def run_gpt(text: str):
+ logger.info(f"Custom GPT prompt: {text}")
+ gpt = chat.ask(text, add_to_context=False)
+ return "".join(list(gpt)), "Response generated"
+
+ def run_tts(text: str, add_to_context: bool):
+ logger.info(f"Custom TTS to queue: {text}")
+ if add_to_context:
+ context.put({"role": "assistant", "content": text})
+
+ def gen():
+ yield text
+
+ tts.synthesize(gen(), loop)
+ return "TTS audio added to queue"
+
+ gpt_input.render()
+ gr.Button(
+ "Ask gpt",
+ ).click(
+ run_gpt,
+ inputs=gpt_input,
+ outputs=[tts_input, gr.Textbox(container=False, interactive=False)],
+ )
+
+ with gr.Group():
+ tts_input.render()
+ context_checkbox = gr.Checkbox(
+ label="Add to context",
+ value=False,
+ )
+ gr.Button(
+ "Add tts to queue",
+ size="lg",
+ variant="primary",
+ ).click(
+ run_tts,
+ inputs=[tts_input, context_checkbox],
+ outputs=gr.Textbox(
+ container=False,
+ interactive=False,
+ ),
+ )
+ with gr.Column():
+ with gr.Group():
+ gr.Textbox(
+ label="Prompt queue",
+ value=lambda: "\n".join(event_handler._queue.all()) or "Empty",
+ interactive=False,
+ every=1,
+ )
+ gr.Button(
+ "Clear prompt queue",
+ size="lg",
+ variant="secondary",
+ ).click(
+ lambda: event_handler._queue.clear(),
+ outputs=gr.Textbox(
+ container=False,
+ interactive=False,
+ ),
+ )
+ with gr.Group():
+ gr.Textbox(
+ label="TTS queue",
+ value=lambda: "\n".join([repr(x) for x in tts.queue.all()]) or "Empty",
+ every=1,
+ )
+ return tab
diff --git a/backend/src/components/tabs/elevenlabs.py b/backend/src/components/tabs/elevenlabs.py
new file mode 100644
index 0000000..939c9f4
--- /dev/null
+++ b/backend/src/components/tabs/elevenlabs.py
@@ -0,0 +1,71 @@
+import gradio as gr
+from src.tts import tts
+from loguru import logger
+
+
+def clone_voice(name: str, files: list[str]):
+ logger.info(f"Cloning voice {name}")
+ msg, voice = tts.clone_voice_from_files(name, files)
+ if voice:
+ return f"New Voice {name} created ID: {voice.voice_id}"
+ else:
+ return f"Error: {msg}"
+
+
+def gen_voices_html():
+ full_html = '
'
+
+ for voice in tts.get_voices():
+ attributes = [
+ ("ID", voice.voice_id),
+ ("Category", voice.category),
+ ("Description", voice.description),
+ ("Labels", voice.labels),
+ ("Samples", voice.samples),
+ ("Design", voice.design),
+ ("Settings", voice.settings),
+ ]
+
+ html = '
'
+ html += f'
{voice.name}
'
+
+ for attr_name, attr_value in attributes:
+ if attr_value is not None:
+ html += f"
{attr_name}: {attr_value}
"
+
+ html += f'
'
+ html += "
"
+ full_html += html
+
+ full_html += "
"
+ return gr.HTML(full_html)
+
+
+def elevenlabs_tab():
+ with gr.Tab("Elevenlabs") as tab:
+ with gr.Tab("Available Voices"):
+ gr.Button("Refresh Voices").click(
+ gen_voices_html,
+ outputs=gr.HTML(),
+ )
+
+ with gr.Tab("Instant Voice Clone"):
+ gr.Interface(
+ fn=clone_voice,
+ inputs=[
+ gr.Textbox(
+ label="Voice name",
+ placeholder="Voz legal 1",
+ ),
+ gr.File(
+ label="Audio files (max 25)",
+ file_count="multiple",
+ file_types=["audio"],
+ type="filepath",
+ ),
+ ],
+ outputs="text",
+ allow_flagging="never",
+ )
+
+ return tab
diff --git a/backend/src/components/tabs/logs.py b/backend/src/components/tabs/logs.py
new file mode 100644
index 0000000..9176f9d
--- /dev/null
+++ b/backend/src/components/tabs/logs.py
@@ -0,0 +1,15 @@
+import gradio as gr
+from io import StringIO
+
+dashboard_sink = StringIO()
+
+def logs_tab():
+ with gr.Tab("Logs") as tab:
+ gr.Code(
+ value=lambda: "\n".join(dashboard_sink.getvalue().splitlines()[-200:]), # type: ignore
+ label="Logs",
+ interactive=False,
+ every=1,
+ language="typescript",
+ )
+ return tab
diff --git a/backend/src/dashboard.py b/backend/src/dashboard.py
index e28a930..322f7e7 100644
--- a/backend/src/dashboard.py
+++ b/backend/src/dashboard.py
@@ -1,385 +1,22 @@
import asyncio
import gradio as gr
import httpx
-from io import StringIO
-from loguru import logger
-from src.config import global_config
-from src.handler import event_handler
-from src.prompts import prompt_manager
-from src.tts import tts
-from src.chatgpt import chat
-from src.context import context
-from src.websocket import ws
-
-dashboard_sink = StringIO()
-
-
-def change_prompt(prompt_id: str, voice_id: str, model: str, clear_context: bool):
- logger.info(f"Setting prompt to {prompt_id} and voice {voice_id}")
- if prompt_id not in list(prompt_manager.prompts):
- return f"Prompt {prompt_id} does not exist"
- global_config.elevenlabs_voice_id = voice_id
- global_config.elevenlabs_model = model
- prompt_manager.set_current_prompt(prompt_id, clear_context)
- r = f"Prompt setted to {prompt_id} with model {model} and voice {voice_id}"
- if clear_context:
- r += " and context cleared"
- return r
-
-
-def get_context_as_chatbot() -> list[tuple[str, str]]:
- full_messages = []
- question_response = []
- for entries in context.all():
- question_response.append(entries["content"])
- if len(question_response) >= 2:
- full_messages.append(question_response)
- question_response = []
- if len(context.all()) % 2 != 0:
- # Make sure all messages are pairs
- full_messages.append(question_response)
- full_messages[-1].append(" ")
- return full_messages
-
-
-def save_prompt(prompt_id: str, prompt: str):
- logger.info(f"Saving prompt {prompt_id}")
- prompt_manager.new_custom_prompt(prompt_id, prompt)
-
-
-async def change_personality(personality_id: str, checkboxes: list):
- clear_context = "Clear context" in checkboxes
- notify_minecraft = "Notify Minecraft" in checkboxes
- logger.info(f"Setting personality to {personality_id}")
-
- if personality_id not in list(prompt_manager.personalities):
- return f"Personality {personality_id} does not exist"
-
- prompt_manager.set_personality(personality_id, clear_context)
- prompt_manager.set_current_prompt(prompt_manager.personalities[personality_id]["prompt_id"], clear_context)
- global_config.elevenlabs_voice_id = prompt_manager.personalities[personality_id]["voice_id"]
- global_config.elevenlabs_model = prompt_manager.personalities[personality_id]["model"]
- if notify_minecraft:
- await ws.broadcast({"action": "new_personality", "data": personality_id})
-
- return f"Personality setted to {personality_id}"
-
-
-def new_personality(personality_id: str, prompt_id: str, model: str, voice_id: str):
- logger.info(f"Creating new personality {personality_id}")
-
- prompt_manager.personalities[personality_id] = {
- "prompt_id": prompt_id,
- "voice_id": voice_id,
- "model": model,
- }
- prompt_manager.save()
- return f"Personality {personality_id} created"
-
-
-def clone_voice(name: str, files: list[str]):
- logger.info(f"Cloning voice {name}")
- msg, voice = tts.clone_voice_from_files(name, files)
- if voice:
- return f"New Voice {name} created ID: {voice.voice_id}"
- else:
- return f"Error: {msg}"
+from src.config import global_config
+from src.components.tabs.elevenlabs import elevenlabs_tab
+from src.components.tabs.customtts import customTTS_tab
+from src.components.tabs.context import context_tab
+from src.components.tabs.logs import logs_tab
+from src.components.tabs.config import config_tab
def start_dashboard(loop: asyncio.AbstractEventLoop):
with gr.Blocks() as blocks:
- with gr.Tab("Custom TTS"):
- with gr.Row():
- with gr.Column(scale=2):
- with gr.Group():
- gpt_input = gr.Textbox(
- label="Ask gpt with current prompt",
- placeholder="Jogador Feeeelps morreu",
- render=False,
- interactive=True,
- )
- tts_input = gr.Textbox(
- label="GPT Output",
- placeholder="Ah, que pena felps morreu.",
- render=False,
- interactive=True,
- )
-
- def run_gpt(text: str):
- logger.info(f"Custom GPT prompt: {text}")
- gpt = chat.ask(text, add_to_context=False)
- return "".join(list(gpt)), "Response generated"
-
- def run_tts(text: str, add_to_context: bool):
- logger.info(f"Custom TTS to queue: {text}")
- if add_to_context:
- context.put({"role": "assistant", "content": text})
-
- def gen():
- yield text
-
- tts.synthesize(gen(), loop)
- return "TTS audio added to queue"
-
- gpt_input.render()
- gr.Button(
- "Ask gpt",
- ).click(
- run_gpt,
- inputs=gpt_input,
- outputs=[tts_input, gr.Textbox(container=False, interactive=False)],
- )
-
- with gr.Group():
- tts_input.render()
- context_checkbox = gr.Checkbox(
- label="Add to context",
- value=False,
- )
- gr.Button(
- "Add tts to queue",
- size="lg",
- variant="primary",
- ).click(
- run_tts,
- inputs=[tts_input, context_checkbox],
- outputs=gr.Textbox(
- container=False,
- interactive=False,
- ),
- )
- with gr.Column():
- with gr.Group():
- gr.Textbox(
- label="Prompt queue",
- value=lambda: "\n".join(event_handler._queue.all()) or "Empty",
- interactive=False,
- every=1,
- )
- gr.Button(
- "Clear prompt queue",
- size="lg",
- variant="secondary",
- ).click(
- lambda: event_handler._queue.clear(),
- outputs=gr.Textbox(
- container=False,
- interactive=False,
- ),
- )
- with gr.Group():
- gr.Textbox(
- label="TTS queue",
- value=lambda: "\n".join([repr(x) for x in tts.queue.all()]) or "Empty",
- every=1,
- )
-
- with gr.Tab("Context"):
- gr.Chatbot(
- value=get_context_as_chatbot,
- every=1,
- container=False,
- height=700,
- layout="panel",
- show_copy_button=True,
- )
-
- with gr.Tab("Logs"):
- gr.Code(
- value=lambda: "\n".join(dashboard_sink.getvalue().splitlines()[-200:]), # type: ignore
- label="Logs",
- interactive=False,
- every=1,
- language="typescript",
- )
-
- with gr.Tab("Config"):
- with gr.Tab("Global Config"):
- gr.Markdown(
- value=lambda: global_config.as_markdown(),
- every=5,
- )
-
- with gr.Tab("Change Personality"):
- gr.Interface(
- fn=change_personality,
- inputs=[
- gr.Textbox(
- label="Personality",
- value=lambda: prompt_manager.current_personality_id,
- ),
- gr.CheckboxGroup(
- label="Clear context",
- choices=["Clear context", "Notify Minecraft"],
- container=False,
- ),
- ],
- outputs="text",
- allow_flagging="never",
- )
-
- with gr.Tab("Personalities"):
-
- def personalities_html():
- return "\n".join(
- [
- f"\n{key}
\n\n{prompt_manager.personalities[key]}\n
\n "
- for key in prompt_manager.personalities
- ]
- )
-
- gr.HTML(
- value=lambda: personalities_html(),
- every=5,
- )
-
- with gr.Tab("New personality"):
- gr.Interface(
- fn=new_personality,
- inputs=[
- gr.Textbox(
- label="Personality name",
- placeholder="personality_name",
- ),
- gr.Textbox(
- label="Prompt ID",
- placeholder="prompt0",
- ),
- gr.Dropdown(
- label="Model ID",
- choices=["eleven_multilingual_v2", "eleven_multilingual_v1"],
- ),
- gr.Textbox(
- label="Voice ID",
- placeholder="9Fa9ozDyMkNFPnyRbRZD",
- ),
- ],
- outputs="text",
- allow_flagging="never",
- )
-
- with gr.Tab("Change Prompt"):
- gr.Interface(
- fn=change_prompt,
- inputs=[
- gr.Textbox(
- label="Prompt ID",
- value=lambda: prompt_manager.current_prompt_id,
- ),
- gr.Textbox(
- label="Voice ID",
- value=lambda: global_config.elevenlabs_voice_id,
- ),
- gr.Dropdown(
- label="Voice Model",
- choices=["eleven_multilingual_v2", "eleven_multilingual_v1"],
- value=lambda: global_config.elevenlabs_model,
- ),
- gr.Checkbox(label="Clear context", value=False),
- ],
- outputs="text",
- allow_flagging="never",
- )
- gr.Textbox(
- value=lambda: f"Current prompt: {prompt_manager.current_prompt_id}\nCurrent voice: {global_config.elevenlabs_voice_id}\nCurrent model: {global_config.elevenlabs_model}",
- interactive=False,
- every=1,
- lines=3,
- max_lines=3,
- container=False,
- )
-
- with gr.Tab("Prompts"):
-
- def prompts_html():
- return "\n".join(
- [
- f"\n{id}
\n\n{prompt}\n
\n "
- for id, prompt in prompt_manager.prompts.items()
- ]
- )
-
- gr.HTML(lambda: prompts_html(), every=5)
-
- with gr.Tab(label="New Prompt"):
- custom_prompt_id = gr.Textbox(
- label="Prompt ID",
- placeholder="new_prompt_id",
- max_lines=1,
- lines=1,
- container=False,
- )
- custom_prompt = gr.Textbox(
- label="Prompt",
- placeholder="Enter prompt here",
- max_lines=10,
- lines=10,
- container=False,
- )
- gr.Button(
- "Save Prompt",
- ).click(
- save_prompt,
- inputs=[custom_prompt_id, custom_prompt],
- )
-
- with gr.Tab("Elevenlabs"):
- with gr.Tab("Available Voices"):
-
- def gen_html():
- full_html = ''
-
- for voice in tts.get_voices():
- attributes = [
- ("ID", voice.voice_id),
- ("Category", voice.category),
- ("Description", voice.description),
- ("Labels", voice.labels),
- ("Samples", voice.samples),
- ("Design", voice.design),
- ("Settings", voice.settings),
- ]
-
- html = '
'
- html += f'
{voice.name}
'
-
- for attr_name, attr_value in attributes:
- if attr_value is not None:
- html += f"
{attr_name}: {attr_value}
"
-
- html += f'
'
- html += "
"
- full_html += html
-
- full_html += "
"
- return gr.HTML(full_html)
-
- gr.Button(
- "Refresh Voices",
- ).click(
- gen_html,
- outputs=gr.HTML(),
- )
-
- with gr.Tab("Instant Voice Clone"):
- gr.Interface(
- fn=clone_voice,
- inputs=[
- gr.Textbox(
- label="Voice name",
- placeholder="Voz legal 1",
- ),
- gr.File(
- label="Audio files (max 25)",
- file_count="multiple",
- file_types=["audio"],
- type="filepath",
- ),
- ],
- outputs="text",
- allow_flagging="never",
- )
+ customTTS_tab(loop)
+ context_tab()
+ logs_tab()
+ config_tab()
+ elevenlabs_tab()
blocks.queue().launch(prevent_thread_lock=True, share=True, quiet=True)
if global_config.discord_webhook_key:
diff --git a/backend/src/main.py b/backend/src/main.py
index faf57b7..2f35097 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -8,7 +8,8 @@
from src.handler import event_handler
from src.models import Config, Event, IncomingEvent
from src.websocket import ws
-from src.dashboard import start_dashboard, dashboard_sink
+from src.dashboard import start_dashboard
+from src.components.tabs.logs import dashboard_sink
# TODO: Add option to enable debug logs to stdout with backtrace and diagnose when developing
logger.remove() # Remove default logger