Compare commits

..

9 Commits

Author SHA1 Message Date
5e3747179f UI prototype 2025-01-15 23:39:33 +01:00
44e5bd423e start cases 2025-01-15 23:39:09 +01:00
03c93f4d8b force encoding 2025-01-15 23:38:50 +01:00
f9c4d3e2db add webserver 2025-01-15 23:38:39 +01:00
7224111a0b python package restructuring 2025-01-14 20:29:29 +01:00
0c022d4731 tuned prompt 2025-01-13 23:33:51 +01:00
a697f49698 whitespace 2025-01-13 23:33:22 +01:00
3218e7eb63 whitespace 2025-01-13 23:32:54 +01:00
ef789375c8 improved append file 2025-01-13 23:32:42 +01:00
39 changed files with 4390 additions and 101 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
*.prof *.prof
__pycache__ __pycache__
*.venv *.venv
*.egg-info

2
.vscode/launch.json vendored
View File

@@ -15,7 +15,7 @@
"name": "PyDebug: __main__.py", "name": "PyDebug: __main__.py",
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"program": "__main__.py", "program": "chatbug/__main__.py",
"console": "integratedTerminal" "console": "integratedTerminal"
} }
] ]

0
chatbug/__init__.py Normal file
View File

View File

@@ -1,6 +1,7 @@
print("running __main__.-py") print("running __main__.-py")
from llama import main from chatbug.llama import main_func
if __name__ == "__main__": if __name__ == "__main__":
main() main_func()

View File

@@ -1,7 +1,7 @@
from inference import Inference from chatbug.inference import Inference
from modelconfig import Modelconfig from chatbug.modelconfig import Modelconfig
def main(): def main():

46
chatbug/file_append.py Normal file
View File

@@ -0,0 +1,46 @@
import os
def check_append_file(prompt: str) -> str:
if "@" in prompt:
parts = prompt.split(" ")
content = []
for part in parts:
if part.startswith("@"):
filename = part[1:]
try:
if os.path.exists(filename):
with open(filename, "r", encoding="utf-8") as f:
content.append("%s:'''\n%s'''" % (filename, f.read()))
except FileNotFoundError:
print(f"File '{filename}' not found.")
except Exception as e:
print("exception encountered %s", e)
content.append(prompt)
return "\n".join(content)
return prompt
if __name__ == "__main__":
exit() # not accidentally trigger it
# Create some sample files
with open("fmain.py", "w") as f:
f.write("# This is main.py\n")
with open("finference.py", "w") as f:
f.write("# This is inference.py\n")
# Test cases
test_prompts = [
"@fmain.py",
"@fmain.py @finference.py",
"@fnonexistent.py",
"@fmain.py @fnonexistent.py"
]
for prompt in test_prompts:
print(f"Testing prompt: {prompt}")
result = check_append_file(prompt)
print(f"Result: {result}")
print("-" * 20)

View File

@@ -1,22 +1,11 @@
import time import time
import json import json
import random import random
from tool_helper import tool_list, parse_and_execute_tool_call from chatbug.tool_helper import tool_list, parse_and_execute_tool_call
from inference import Inference, torch_reseed from chatbug.inference import Inference, torch_reseed
from chatbug.file_append import check_append_file
def check_append_file(prompt: str) -> str:
if prompt.startswith("@"):
prompt = prompt[1:] # Remove the '@'
filename = prompt.split(" ")[0]
try:
with open(filename, "r") as f:
content = f.read()
return "'''%s'''\n\n%s" % (content, prompt)
except:
print(f"File '{filename}' not found.")
return prompt
def msg(role: str, content: str) -> dict: def msg(role: str, content: str) -> dict:
return {"role": role, "content": content} return {"role": role, "content": content}
@@ -84,22 +73,22 @@ class Terminal:
print("") print("")
elif input_text.startswith("/history"): elif input_text.startswith("/history"):
history = self.inference.tokenize(self.message, tokenize=False) history = self.inference.tokenize(self.messages, tokenize=False)
# history = tokenizer.apply_chat_template(self.message, return_tensors="pt", tokenize=False, add_generation_prompt=False) # history = tokenizer.apply_chat_template(self.message, return_tensors="pt", tokenize=False, add_generation_prompt=False)
print(history) print(history)
elif input_text.startswith("/undo"): elif input_text.startswith("/undo"):
if len(self.message) > 2: if len(self.messages) > 2:
print("undo latest prompt") print("undo latest prompt")
self.message = self.message[:-2] self.message = self.messages[:-2]
else: else:
print("cannot undo because there are not enough self.message on history.") print("cannot undo because there are not enough self.message on history.")
print("") print("")
elif input_text.startswith("/regen"): elif input_text.startswith("/regen"):
if len(self.message) >= 2: if len(self.messages) >= 2:
print("regenerating message (not working)") print("regenerating message (not working)")
self.message = self.message[:-1] self.messages = self.messages[:-1]
seed = random.randint(0, 2**32 - 1) # Generate a random seed seed = random.randint(0, 2**32 - 1) # Generate a random seed
torch_reseed(seed) torch_reseed(seed)
self.append_generate_chat(None) self.append_generate_chat(None)
@@ -119,8 +108,8 @@ class Terminal:
self.append_generate_chat(content) self.append_generate_chat(content)
elif input_text.startswith("/auto"): elif input_text.startswith("/auto"):
message_backup = self.message message_backup = self.messages
self.message = [self.roleflip] self.messages = [self.roleflip]
for m in self.message_backup: for m in self.message_backup:
role = m["role"] role = m["role"]
content = m["content"] content = m["content"]
@@ -157,7 +146,7 @@ class Terminal:
elif input_text.startswith("/load"): elif input_text.startswith("/load"):
with open("messages.json", "r") as f: with open("messages.json", "r") as f:
new_messages = json.load(f) new_messages = json.load(f)
messages = [self.messages[0]] + new_messages[1:] self.messages = [self.messages[0]] + new_messages[1:]
elif input_text.startswith("/help"): elif input_text.startswith("/help"):
print("!<prompt> answer as 'tool' in <tool_response> tags") print("!<prompt> answer as 'tool' in <tool_response> tags")

View File

@@ -14,10 +14,10 @@ from transformers.cache_utils import (
) )
import torch import torch
import time import time
import utils
import re import re
import os import os
from modelconfig import Modelconfig import chatbug.utils as utils
from chatbug.modelconfig import Modelconfig
torch.set_num_threads(os.cpu_count()) # Adjust this to the number of threads/cores you have torch.set_num_threads(os.cpu_count()) # Adjust this to the number of threads/cores you have

View File

@@ -1,9 +1,9 @@
from inference import Inference
from modelconfig import Modelconfig
import time import time
import nvidia_smi import nvidia_smi
import torch import torch
import gc import gc
from chatbug.inference import Inference
from chatbug.modelconfig import Modelconfig
def empty_cuda(): def empty_cuda():

View File

@@ -1,10 +1,11 @@
from tool_helper import tool_list
from tool_functions import register_dummy
from inference import Inference
import datetime import datetime
import model_selection from chatbug.tool_helper import tool_list
from generation_loop import Terminal, msg from chatbug.tool_functions import register_dummy
from chatbug.inference import Inference
from chatbug.generation_loop import Terminal, msg
from chatbug import model_selection
register_dummy() register_dummy()
@@ -13,7 +14,7 @@ register_dummy()
def initialize_config(inference: Inference) -> Terminal: def initialize_config(inference: Inference) -> Terminal:
# systemmessage at the very begin of the chat. Will be concatenated with the automatic tool usage descriptions # systemmessage at the very begin of the chat. Will be concatenated with the automatic tool usage descriptions
system_prompt = "Hold a casual conversation with the user. Keep responses short at max 5 sentences and on point. Answer using markdown to the user. When providing code examples, avoid comments which provide no additional information." system_prompt = "Hold a casual conversation with the user. Keep responses short at max 5 sentences and on point. Answer using markdown to the user. When providing code examples, avoid comments which provide no additional information. Do not summarize."
current_date_and_time = datetime.datetime.now().strftime("Current date is %Y-%m-%d and its %H:%M %p right now.") current_date_and_time = datetime.datetime.now().strftime("Current date is %Y-%m-%d and its %H:%M %p right now.")
append_toolcalls = False append_toolcalls = False
if append_toolcalls: if append_toolcalls:
@@ -35,9 +36,11 @@ def initialize_config(inference: Inference) -> Terminal:
return terminal return terminal
def main_func():
if __name__ == "__main__":
inference = Inference(model_selection.get_model()) inference = Inference(model_selection.get_model())
terminal = initialize_config(inference) terminal = initialize_config(inference)
terminal.join() terminal.join()
if __name__ == "__main__":
main_func()

View File

@@ -0,0 +1,3 @@
from chatbug.matheval import ast
from chatbug.matheval import interpreter
from chatbug.matheval import lexer

View File

@@ -1,6 +1,5 @@
from chatbug.matheval import lexer
import math_lexer as lexer from chatbug.matheval.lexer import Token
from math_lexer import Token
class Statement: class Statement:

View File

@@ -1,10 +1,11 @@
import math_ast as ast
from sympy.parsing.sympy_parser import parse_expr from sympy.parsing.sympy_parser import parse_expr
from sympy.core.numbers import Integer, One, Zero from sympy.core.numbers import Integer, One, Zero
from sympy import symbols, Eq, solveset, linsolve, nonlinsolve from sympy import symbols, Eq, solveset, linsolve, nonlinsolve
from sympy.core.symbol import Symbol from sympy.core.symbol import Symbol
from chatbug.matheval import ast
def interpret(statement: ast.Statement) -> str: def interpret(statement: ast.Statement) -> str:

View File

@@ -1,5 +1,5 @@
from modelconfig import Modelconfig from chatbug.modelconfig import Modelconfig

View File

@@ -1,10 +1,8 @@
import random import random
import datetime import datetime
from tool_helper import tool from chatbug.tool_helper import tool
import math_lexer import chatbug.matheval as matheval
import math_ast import chatbug.utils as utils
import math_interpreter
import utils
# @tool # @tool
@@ -39,10 +37,10 @@ def math_evaluate(expression: str):
Args: Args:
expression: A valid arithmetic expression (e.g., '2 + 3 * 4'). The expression must not contain '='.""" expression: A valid arithmetic expression (e.g., '2 + 3 * 4'). The expression must not contain '='."""
try: try:
tokens = math_lexer.tokenize(expression) tokens = matheval.lexer.tokenize(expression)
parser = math_ast.Parser() parser = matheval.ast.Parser()
ast = parser.parse(tokens) ast = parser.parse(tokens)
return math_interpreter.interpret(ast) return matheval.interpreter.interpret(ast)
except Exception as e: except Exception as e:
utils.print_error("Tool call evaluation failed. - " + str(e)) utils.print_error("Tool call evaluation failed. - " + str(e))
return "Tool call evaluation failed." return "Tool call evaluation failed."
@@ -58,10 +56,10 @@ Args:
expression = "solve " + " and ".join(equations) + " for " + " and ".join(variables) expression = "solve " + " and ".join(equations) + " for " + " and ".join(variables)
print(expression) print(expression)
tokens = math_lexer.tokenize(expression) tokens = matheval.lexer.tokenize(expression)
parser = math_ast.Parser() parser = ast.Parser()
ast = parser.parse(tokens) ast = parser.parse(tokens)
return math_interpreter.interpret(ast) return matheval.interpreter.interpret(ast)
except Exception as e: except Exception as e:
utils.print_error("Tool call evaluation failed. - " + str(e)) utils.print_error("Tool call evaluation failed. - " + str(e))
return "Tool call evaluation failed." return "Tool call evaluation failed."

View File

@@ -2,7 +2,7 @@
from typing import Callable, List, Optional from typing import Callable, List, Optional
import json import json
import re import re
import utils import chatbug.utils as utils
tool_list = [] tool_list = []

0
chatbug/ui/__init__.py Normal file
View File

20
chatbug/ui/__main__.py Normal file
View File

@@ -0,0 +1,20 @@
from .server import start_server
from .serverwait import wait_for_server
from .ui import start_ui, _start_sandboxed
def start_ui():
svr = start_server(start_thread=False)
url = f"http://localhost:{svr.port}"
# wait_for_server(url)
# # start_ui(threaded=False)
# import webview
# w = webview.create_window('asdf', '../../web/index.html', min_size=(1200, 900), zoomable=True)
# webview.start(ssl=True)
if __name__ == "__main__":
start_ui()

3771
chatbug/ui/bottle.py Normal file

File diff suppressed because it is too large Load Diff

50
chatbug/ui/bottle_svr.py Normal file
View File

@@ -0,0 +1,50 @@
#tornado needs this or it does not run
import asyncio
try:
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
except AttributeError:
print("Probably running on linux")
from bottle import route, run, response, static_file, request, post
from .file_watchdog import FileWatchdog
class BottleServer:
def __init__(self, listen="0.0.0.0", port=8080, start_thread=True, root="web"):
self.root = root
self.port = port
self.listen = listen
self.wdt = FileWatchdog(self.root)
if start_thread:
import threading
self.thread = threading.Thread(target=self._run, args=())
self.thread.name = "BottleServerThread"
self.thread.daemon = True
self.thread.start()
else:
self._run()
def _home(self):
return static_file("index.html", root= self.root)
def _watchdog(self):
return str(self.wdt.time)
def _files(self, name):
if name.endswith(".vue"):
return static_file(name, root= self.root, mimetype="text/html")
return static_file(name, root= self.root)
def _run(self):
route('/')(self._home)
route('/watchdog')(self._watchdog)
route('/<name:path>')(self._files)
print(f"Starting server at {self.listen}:{self.port}")
run(host=self.listen, port=self.port, debug=False, threaded=True, quiet=True)

View File

@@ -0,0 +1,47 @@
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class MyHandler(FileSystemEventHandler):
def __init__(self, function):
self.function = function
def on_any_event(self, _event):
# Handle the event (e.g., file created, modified, deleted)
self.function()
class FileWatchdog:
def __init__(self, path):
self.path = path
self.time = 0
event_handler = MyHandler(lambda: self.event_handler())
self.observer = Observer()
self.observer.schedule(event_handler, path, recursive=True)
self.observer.start()
def event_handler(self):
#print("change detected")
self.time = time.time()
def stop(self):
self.observer.stop()
if __name__ == "__main__":
wdt = FileWatchdog("./web")
try:
while True:
time.sleep(1)
print(wdt.time)
except KeyboardInterrupt:
wdt.stop()

10
chatbug/ui/server.py Normal file
View File

@@ -0,0 +1,10 @@
from .bottle_svr import BottleServer
def start_server(start_thread=False):
print("server start")
return BottleServer(start_thread=start_thread, root="web")
if __name__ == "__main__":
start_server()

29
chatbug/ui/serverwait.py Normal file
View File

@@ -0,0 +1,29 @@
import time
import requests
import socket
def wait_for_server(url, timeout=10, retry_interval=0.5):
"""
Waits for a web server to become available by polling its URL.
"""
start_time = time.monotonic()
while time.monotonic() - start_time < timeout:
try:
# First, try a simple TCP connection to check if the port is open
hostname, port = url.split("//")[1].split(":")
port = int(port)
with socket.create_connection((hostname, port), timeout=retry_interval):
pass # If the connection succeeds, continue to the HTTP check
# Then, make an HTTP request to ensure the server is responding correctly
response = requests.get(url, timeout=retry_interval)
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
return # Server is up and responding correctly
except (requests.exceptions.RequestException, socket.error) as e:
print(f"Server not yet available: {e}. Retrying in {retry_interval} seconds...")
time.sleep(retry_interval)
raise TimeoutError(f"Server at {url} did not become available within {timeout} seconds.")

30
chatbug/ui/ui.py Normal file
View File

@@ -0,0 +1,30 @@
import webview
from threading import Thread
def start_ui(threaded=False):
if threaded:
_start_threaded()
else:
_start_normal()
def _start_threaded():
t = Thread(target=start_ui, args=[False])
t.run()
def _start_normal():
webview.create_window('Geargenerator', 'http://localhost:8080', min_size=(1200, 900), zoomable=True)
webview.start()
def _start_sandboxed():
webview.create_window('Geargenerator', 'web_v2/geargenerator.html', min_size=(1200, 900), zoomable=True)
webview.start(ssl=True)
if __name__ == "__main__":
_start_sandboxed()
# start_ui(threaded=False)

28
setup.py Normal file
View File

@@ -0,0 +1,28 @@
from setuptools import setup, find_packages
setup(
name='chatbug',
version='0.1.0',
description='A conversational AI chatbot',
author='Florin Tobler',
author_email='florin.tobler@hotmail.com',
packages=find_packages(exclude=["tests"]),
install_requires=[
'transformers',
'accelerate',
'bitsandbytes',
'pytest',
'pywebview',
],
entry_points={
'console_scripts': [
'chatbug=chatbug.llama:main_func',
# a^ b^ c^ d^
# a => the command line argument
# b => the package name
# c => the file name in the package (same as imports)
# d => the function to call
'chatbugui=chatbug.ui.__main__:start_ui',
],
},
)

View File

@@ -1 +0,0 @@
# empty

View File

@@ -1,32 +1,20 @@
import pytest import pytest
import tests.helper as helper from tests import helper
inference = None inference = None
InferenceClass = None
Tensor = None Tensor = None
def prepare(): def prepare():
if InferenceClass == None:
test_import_inference_module_librarys()
if inference == None:
test_instantiate_inference_instance()
def test_import_inference_module_librarys():
import inference
import torch
global InferenceClass
global Tensor
InferenceClass = inference.Inference
Tensor = torch.Tensor
def test_instantiate_inference_instance():
if InferenceClass == None:
test_import_inference_module_librarys()
global inference global inference
inference = InferenceClass() global Tensor
if inference == None:
from torch import Tensor as _Tensor
from chatbug.inference import Inference
from chatbug.model_selection import get_model
inference = Inference(get_model())
Tensor = _Tensor
def test_tool_header_generation(): def test_tool_header_generation():

View File

@@ -1,6 +1,6 @@
import pytest import pytest
import tool_helper import chatbug.tool_helper as tool_helper
import tests.helper as helper from tests import helper

View File

@@ -1,6 +1,6 @@
import pytest import pytest
import tool_functions import chatbug.tool_functions as tool_functions
from tests import helper
def test_math_evaluate_1(): def test_math_evaluate_1():
@@ -28,6 +28,13 @@ def test_math_evaluate_5():
result = tool_functions.math_evaluate("sin(pi/2) + cos(0)") result = tool_functions.math_evaluate("sin(pi/2) + cos(0)")
assert result == "sin(pi/2) + cos(0) = 2" assert result == "sin(pi/2) + cos(0) = 2"
def test_math_evaluate_solve_a():
result = tool_functions.math_evaluate("solve 240=x*r+x*r^2+x*r^3+s and r=1.618 and s=5 for x, r, s")
assert result == "Solved equation system 240 = r**3*x + r**2*x + r*x + s, r = 1.61800000000000 and s = 5 for x=27.7393327937747=~27.739, r=1.61800000000000=~1.618 and s=5.00000000000000=~5.000."
def test_math_evaluate_solve_b():
result = tool_functions.math_evaluate("solve 250=x+x*r+s and r=1.618 and s=0 for x, r, s")
assert result == "Solved equation system 250 = r*x + s + x, r = 1.61800000000000 and s = 0 for x=95.4927425515661=~95.493, r=1.61800000000000=~1.618 and s=0."
@@ -54,4 +61,3 @@ def test_math_solver_3b():
def test_math_solver_4(): def test_math_solver_4():
result = tool_functions.math_evaluate("solve 2*x**3 + 3*y = 7 and x - y = 1 for x, y") result = tool_functions.math_evaluate("solve 2*x**3 + 3*y = 7 and x - y = 1 for x, y")
assert result == "Solved equation system 2*x**3 + 3*y = 7 and x - y = 1 for x=~1.421 and y=~0.421." assert result == "Solved equation system 2*x**3 + 3*y = 7 and x - y = 1 for x=~1.421 and y=~0.421."

View File

@@ -1,7 +1,8 @@
import pytest import pytest
import tool_helper from chatbug import tool_helper
from unittest import mock from unittest import mock
import tests.helper as helper from tests import helper
import re
@@ -40,34 +41,34 @@ def test_match_and_extract_matching3_with_newline():
def test_string_malformed_faulty(): def test_string_malformed_faulty():
with mock.patch("utils.print_error") as print_error_mock: with mock.patch("chatbug.utils.print_error") as print_error_mock:
result = tool_helper._execute_tool_call_str("{json_content}", []) result = tool_helper._execute_tool_call_str("{json_content}", [])
assert result == None assert result == None
print_error_mock.assert_called_once() # this will check if the mocked function on the context was called. print_error_mock.assert_called_once() # this will check if the mocked function on the context was called.
def test_tool_call_json_1(): def test_tool_call_json_1():
with mock.patch("utils.print_error") as print_error_mock: with mock.patch("chatbug.utils.print_error") as print_error_mock:
result = tool_helper._execute_tool_call_json({"name": "tool_dummy", "arguments": {"a": 1, "b": "zwei"}}, [helper.tool_dummy, helper.tool_dummy2]) result = tool_helper._execute_tool_call_json({"name": "tool_dummy", "arguments": {"a": 1, "b": "zwei"}}, [helper.tool_dummy, helper.tool_dummy2])
assert result == "result_1_zwei" assert result == "result_1_zwei"
assert print_error_mock.call_count == 0 assert print_error_mock.call_count == 0
def test_tool_call_json_2(): def test_tool_call_json_2():
with mock.patch("utils.print_error") as print_error_mock: with mock.patch("chatbug.utils.print_error") as print_error_mock:
result = tool_helper._execute_tool_call_json({"name": "tool_dummy2", "arguments": {"text": "some_text"}}, [helper.tool_dummy, helper.tool_dummy2]) result = tool_helper._execute_tool_call_json({"name": "tool_dummy2", "arguments": {"text": "some_text"}}, [helper.tool_dummy, helper.tool_dummy2])
assert result == "SOME_TEXT" assert result == "SOME_TEXT"
assert print_error_mock.call_count == 0 assert print_error_mock.call_count == 0
def test_tool_call_json_non_existing_call_check(): def test_tool_call_json_non_existing_call_check():
with mock.patch("utils.print_error") as print_error_mock: with mock.patch("chatbug.utils.print_error") as print_error_mock:
result = tool_helper._execute_tool_call_json({"name": "tool_dummy_which_is_not_existing", "arguments": {"text": "some_text"}}, [helper.tool_dummy, helper.tool_dummy2]) result = tool_helper._execute_tool_call_json({"name": "tool_dummy_which_is_not_existing", "arguments": {"text": "some_text"}}, [helper.tool_dummy, helper.tool_dummy2])
assert result == None assert result == None
assert print_error_mock.call_count == 1 # this will check if the mocked function on the context was called. assert print_error_mock.call_count == 1 # this will check if the mocked function on the context was called.
def test_tool_call_json_wrong_arguments_check(): def test_tool_call_json_wrong_arguments_check():
with mock.patch("utils.print_error") as print_error_mock: with mock.patch("chatbug.utils.print_error") as print_error_mock:
result = tool_helper._execute_tool_call_json({"name": "tool_dummy", "arguments": {"a": "must_be_an_int_but_is_string", "b": "zwei"}}, [helper.tool_dummy, helper.tool_dummy2]) result = tool_helper._execute_tool_call_json({"name": "tool_dummy", "arguments": {"a": "must_be_an_int_but_is_string", "b": "zwei"}}, [helper.tool_dummy, helper.tool_dummy2])
assert result == None assert result == None
assert print_error_mock.call_count == 1 # this will check if the mocked function on the context was called. assert print_error_mock.call_count == 1 # this will check if the mocked function on the context was called.
@@ -75,7 +76,6 @@ def test_tool_call_json_wrong_arguments_check():
def test_regex_multiline(): def test_regex_multiline():
import re
pattern = r"<start>(.*)</end>" pattern = r"<start>(.*)</end>"
# The text to search (spanning multiple lines) # The text to search (spanning multiple lines)

61
web/index.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> -->
<link rel="stylesheet" href="stylesheet.css">
<script src="alpine.min.js"></script>
<script src="main.js"></script>
<script src="watchdog.js"></script>
</head>
<body>
<div class="sidebar">
<h1>Chatbug 🪲</h1>
<div class="button">🐛 New Chat</div>
<div class="title">Today</div>
<div class="button">Building Web UI with Bottle & Alpine.js</div>
<div class="button">Coding in python</div>
<div class="title">Last Week</div>
<div class="title">Older</div>
</div>
<div class="mainarea">
<!-- <h1 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"></h1> -->
<div class="message">
<div class="bubble">Hello world</div>
</div>
<div class="response">
<div class="">Hello! Nice to meet you. What's up?</div>
</div>
<div class="message">
<div class="bubble">ah, just holding an example conversation with you</div>
</div>
<div class="response">
<div class="">Got it! Fun stuff. What kind of projects are you working on these days?</div>
</div>
<div class="message">
<div class="bubble">LLM chatbot named chatbug 🪲</div>
</div>
<div class="response">
<div class="">Cool name! Chatbug sounds like a friendly one. How's it going?</div>
</div>
<div class="message">
<div class="bubble">making a web ui with bottle and alpinejs</div>
</div>
<div class="input">
<!-- toolbutton for tool submenu, normally hidden unless pressed -->
<div class="button">+</div>
<div class="tool list" style="display:none">
<div class="tool button">attach file</div>
<div class="tool button">regenerate</div>
<div class="tool button">undo</div>
</div>
<input type="text">
<!-- send -->
<div class="button"></div>
</div>
</div>
</body>
</html>

25
web/main.js Normal file
View File

@@ -0,0 +1,25 @@
// import {createApp, ref, reactive} from 'vue';
// const app = createApp({
// data() {
// let msg = ref("hello world")
// try {
// msg.value = "" + pywebview.api
// } catch (e) {
// msg.value = "did not invoke " + e
// }
// window.msg = msg
// return {
// message: msg
// };
// }
// });
// app.mount('#app');

117
web/stylesheet.css Normal file
View File

@@ -0,0 +1,117 @@
body {
background-color: black;
color: white;
font-family: Arial, Helvetica, sans-serif;
margin: 0px;
height: 100%;
}
.sidebar {
width: 250px;
background-color: #2a262a;
float: left;
height: 100%;
position: absolute;
}
.sidebar h1 {
margin: 20px;
}
.sidebar .title {
font-size: 8pt;
margin: 20px;
margin-top: 30px;
margin-bottom: 10px;
}
.sidebar .button {
margin-left: 10px;
margin-right: 10px;
padding: 10px;
border-radius: 10px;
}
.sidebar .button:hover {
background-color: #423a42;
}
.mainarea {
margin-left: 260px;
height: 100%;
position: absolute;
right: 0;
left: 0;
}
.message {
display: flex;
margin-left: 40px;
margin-right: 10px;
}
.bubble {
padding: 10px;
border-radius: 10px;
background-color: #416146;
margin-left: auto;
float: right;
position: relative;
}
.response {
display: flex;
margin: 30px;
position: relative;
}
.response::before {
content: '🪲';
position: absolute;
top: -4px;
left: -30px;
}
.input {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #2a262a;
border-radius: 10px;
width: 70%;
margin: auto;
position: absolute;
bottom: 40px;
}
.tool.list {
display: none;
background-color: #fff;
border: 1px solid #ccc;
position: absolute;
top: 100%;
left: 0;
z-index: 1;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.tool.button {
cursor: pointer;
padding: 5px 10px;
margin: 5px;
}
.input input {
flex-grow: 1;
padding: 10px;
border: 0px solid #ccc;
background: none;
color: white;
}
.input input:focus {
outline: 0px solid black; /* Custom focus outline */
}

67
web/watchdog.js Normal file
View File

@@ -0,0 +1,67 @@
wdt = {
last_wdt_time: 0,
watchdog_counter: 0
}
pollFileChange = () => {
setTimeout(() => {
wdt.watchdog_counter++
console.log(wdt.watchdog_counter)
if (wdt.watchdog_counter > 20) {
return
}
ajax({
type: "GET",
url: "/watchdog",
success: (data) => {
var time = Number(data)
if (wdt.last_wdt_time == 0) {
wdt.last_wdt_time = time
pollFileChange()
} else if (time > wdt.last_wdt_time) {
location.reload();
} else {
pollFileChange()
}
},
})
}, 10000)
}
function ajax(setting) {
if (typeof(shutdown) !== 'undefined') return
var request = new XMLHttpRequest();
request.open(setting.type, setting.url, true);
request.setRequestHeader('Content-Type', setting.dataType)
request.onload = function(data) {
if (typeof(shutdown) !== 'undefined') return
if (this.status >= 200 && this.status < 400) {
if (setting.success) {
setting.success(this.response)
}
} else {
if (setting.error) {
setting.error(this.response)
}
}
}
request.onerror = function(data) {
if (typeof(shutdown) !== 'undefined') return
if (setting.error) {
setting.error(data)
}
}
if (setting.data) {
request.send(setting.data)
} else {
request.send()
}
}
pollFileChange()