Building SolveIt in SolveIt

I give you a challenge: can you build SolveIt in SolveIt in SolveIt?

Agents, Approaching AI, LLMs, SolveIt, Programming, HTML
Reimplementing a light version of SolveIt with FastHTML, Basecoat, and within SolveIt.
Author

Salman Naqvi

Published

Monday, 20 January 2025

This app is built on the techniques taught in Lessons 4, 5, and 6 of the SolveIt course led by Jeremy Howard who created the first modern LLM in 2018, and Johnothan Whitaker. SolveIt is also a brilliant problem-solving framework and interactive platform, that puts you in control–not the LLM.

Also thanks to Rens Dimmendaal for his dialog on how to deploy an app to Plash from SolveIt.

This blog post is a rendered documentation of a very, very basic reimplementation of SolveIt using FastHTML as the web framework. The main goal of this exercise was to exercise my web development and HTMX skillz. Play with the app at this link. Or watch the brief overview below.

SolveIt is an IDE of sorts that’s not only tailored towards coding, but a variety of other tasks from writing to problem solving to planning to learning to exploring and what not. Its premise revolves around the LLM putting the human in control, and thereby boosting human performance and capability. A core part of why SolveIt works so well is that your work is the LLM context.

Many parts of this app can still be improved, refactored, or made more efficient. Play with the app at this link.

Features include: - Code/LLM/Note cells - Context aware LLM - Restore functionality - Create multiple dialogs - Rename dialogs - Chinese/English localization

!git status
On branch main

Your branch is up to date with 'origin/main'.



Changes not staged for commit:

  (use "git add <file>..." to update what will be committed)

  (use "git restore <file>..." to discard changes in working directory)

    modified:   29_lets_build_an_ai_model.ipynb



Untracked files:

  (use "git add <file>..." to include in what will be committed)

    ../images/31_solveit_in_solveit/

    31_solveit_in_solveit.ipynb



no changes added to commit (use "git add" and/or "git commit -a")

Settings and Headers

::: {#243ffab3 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:19:53.056034+00:00’}

from fasthtml.common import *

:::

::: {#f9ccd095 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:19:53.559518+00:00’}

bsc_hdrs = (
    Script(src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"),  # Need to download first.
    Link(rel='stylesheet', href='https://cdn.jsdelivr.net/npm/basecoat-css@0.3.6/dist/basecoat.cdn.min.css'),
    Script(src='https://cdn.jsdelivr.net/npm/basecoat-css@0.3.6/dist/js/all.min.js', defer=True),
)

:::

::: {#7d1e280a .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:19:53.916338+00:00’}

app = FastHTML(hdrs=(bsc_hdrs, MarkdownJS(), HighlightJS(langs=['python'])), session_cookie='mysessioncookie', debug=True)
rt = app.route

:::

A custom session cookie needs to be set for FastHTML sessions to work in SolveIt as SolveIt currently uses the default one.

预览 // Preview

Here I create a preview helper function that will allow me to interactively preview various components of the app.

from functools import partial
from fasthtml.jupyter import *
server = JupyUvi(app)
def get_preview(app):
    return partial(HTMX, app=app, host=None, port=None)
prev = get_preview(app); prev()
render_ft()

Database

Creating a database to store users and their dialogs, with dialogs linked to users via uid.

::: {#1f39f676 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:19:59.864649+00:00’}

from fastlite import *

:::

::: {#88d6aa36 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:00.225034+00:00’}

db = database('solvish.db')

:::

db
<Database <apsw.Connection object "/app/data/30–39 学习/33 人工智能/33.20 SolveIt/第五课/solvish/solvish.db" at 0x7abddf8c4500>>

::: {#dd112d66 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:01.614856+00:00’}

class User: id:int; restore_code:str
class Dialog: id:int; uid:int; dialog_num:int; name:str; messages:str; server_session:str

:::

::: {#6bd3be8e .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:02.309831+00:00’}

users = db.create(User, transform=True)
dialogs = db.create(Dialog, pk=('uid', 'dialog_num'), transform=True)

:::

Tables will not be overwritten as db.create() is idempotent.

users.delete_where()
dialogs.delete_where()
<Table dialog (id, uid, dialog_num, name, messages, server_session)>

.delete_where with no arguments passed will drop all rows from the table.

New Dialog

Creating a new dialog first auto-increments dialog_num based on the user’s existing dialogs, then sets session['current_dialog'] to track which dialog is active. The dialog is stored in the dialogs table for persistence across sessions, while also being loaded into user_chats as a live chat object for the current server session.

::: {#b7b8f919 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:06.558131+00:00’}

from lisette.core import *
from toolslm.shell import get_shell

sp = 'Respond in the language the user uses; The user has access to a Python interpreter that you can view; NEVER copy the format of the code interpreter messsages in your response.'
def make_chat(hist=[]): return Chat('moonshot/kimi-k2-0711-preview', hist=hist, sp=sp), get_shell()

:::

make_chat()
(<lisette.core.Chat at 0x7abddf926ea0>,
 <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abddf9486e0>)

As my Kimi API key is compatable with servers in China, I have to change the Moonshoot endpoint.

::: {#35143833 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:09.693134+00:00’}

os.environ['MOONSHOT_API_BASE'] = 'https://api.moonshot.cn/v1'

:::

make_chat()[0]('what do you have access to?')

I can see the same Python interpreter you’re using.
Whenever you run code in it, the full session—inputs, outputs, errors, variables, imports, even matplotlib plots—is visible to me in real time, so I can comment on or debug it without you having to paste anything.

  • id: chatcmpl-6969924cd204c1cd8cbbd130
  • model: moonshot/kimi-k2-0711-preview
  • finish_reason: stop
  • usage: Usage(completion_tokens=60, prompt_tokens=54, total_tokens=114, completion_tokens_details=None, prompt_tokens_details=None)

::: {#50efa44f .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:53:03.375363+00:00’}

from coolname import generate_slug

@rt
def create_dialog(session):
    uid = session.get('userid')
    existing = dialogs(where=f'uid={uid}')
    dnum = max((d.dialog_num for d in existing), default=0) + 1
    session['current_dialog'] = dnum
    dialogs.insert(uid=uid, dialog_num=dnum, name=generate_slug(2), messages='[]')
    user_chats[(uid,dnum)] = make_chat()
    return Div(hx_get=f'/dialog?dnum={dnum}', hx_trigger='load', hx_target='#content', hx_swap='innerHTML')

:::

Session Persistance

::: {#67b4dc4e .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:16.610315+00:00’}

user_chats = {}

:::

I’ll be handling session persistance with this user_chats dictionary. If my server restarts, it means user_chats will be erased and thus a new session will be created for all users. Users can input their restore code to retrieve back their data.

::: {#89ceda80 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:18.625082+00:00’}

def ensure_user(session):
    uid = session.get('userid')
    if not uid or uid not in users:
        u = users.insert(restore_code=generate_slug(3))
        session['userid'] = u.id
    return session['userid']

:::

fake_session = {}
uid = ensure_user(fake_session)
fake_session, uid
({'userid': 1}, 1)

::: {#9ac5a9ef .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:20.500481+00:00’}

from fastcore.all import *
import json

def ensure_dialog(session):
    uid, dnum = map(session.get, ('userid', 'current_dialog'))
    if (uid, dnum) in user_chats: return
    elif dnum and (uid, dnum) in dialogs: 
        d = dialogs[(uid, dnum)]
        user_chats[(uid, dnum)] = make_chat(json.loads(d.messages))
    else: create_dialog(session)

:::

ensure_dialog acts as a lazy loader for chat sessions. It first checks whether the user’s current dialog is already active in memory—if so, there’s nothing to do. If the dialog exists in the database but hasn’t been loaded yet, it retrieves the stored messages and reconstructs the chat object. Otherwise, when no dialog exists at all, it creates a fresh one for the user.

ensure_dialog(fake_session)
user_chats
{(1, 1): (<lisette.core.Chat at 0x7abdd55438f0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5351730>)}

::: {#f4bf5236 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:22.306865+00:00’}

from contextvars import ContextVar
current_lang = ContextVar('lang', default='en'); current_lang
<ContextVar name='lang' default='en' at 0x7abdd518eb10>

:::

Every time a request is made, a new ContextVar is created. This is a way I can keep language preferences seperate for each user.

::: {#f4830cbf .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:23.395485+00:00’}

@rt
def setsession(session):
    current_lang.set(session.setdefault('lang', 'en'))
    uid = ensure_user(session)
    ensure_dialog(session)

:::

prev(setsession)

The cookie only stores the user id. The data for the user is stored inside the current Python runtime. Once the runtime ends, the data is lost. The database permanently stores everything.

users()
[User(id=1, restore_code='sparkling-ochre-copperhead')]
dialogs()
[Dialog(id=None, uid=1, dialog_num=1, name='fine-beetle', messages='[]', server_session=None),
 Dialog(id=None, uid=1, dialog_num=2, name='fat-junglefowl', messages='[]', server_session=None)]
user_chats
{(1, 1): (<lisette.core.Chat at 0x7abdd55438f0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5351730>),
 (1, 2): (<lisette.core.Chat at 0x7abdd52451c0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5245100>)}
@rt
def debug(session):
    return Pre(str(dict(session)))
prev(debug)
@rt
def reset(session):
    session.clear()
    users.delete_where()
    dialogs.delete_where()
    return session

I won’t be exporting these two routes into the final application, else it’d allow users to mess around with other users!

Localization

A translation dictionary stores the strings for both Chinese and English. The helper function t() performs the localization.

::: {#95173331 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ hide_input=‘true’ time_run=‘2026-01-16T01:20:31.904491+00:00’}

T = {
    'zh': {
        'input_placeholder': '输入内容···',
        'submit': '提交',
        'ask_model': '问问模型···',
        'your_code': '你的代码···',
        'any_thoughts': '有啥想法···',
        'enter_code': '输入代码···',
        'code': '代码',
        'prompt': '题词',
        'note': '笔记',
        'tab': '标签',
        'all_dialogs': '所有对话',
        'new_dialog': '新对话',
        'delete': '删除',
        'save': '保存',
        'solveit_lite': 'solveit-轻量版',
        'footer': '由 FastHTML 匠心打造',
        'restore_code': '恢复码',
        'restore_failed': '‼ 恢复失败',
        'restore_error': '找不到与此恢复码关联的账户,请检查后重试。',
        'enter_restore_code': '输入恢复码···',
    },
    'en': {
        'input_placeholder': 'Enter content...',
        'submit': 'Submit',
        'ask_model': 'Ask the model...',
        'your_code': 'Your code...',
        'any_thoughts': 'Any thoughts...',
        'enter_code': 'Enter code...',
        'code': 'Code',
        'prompt': 'Prompt',
        'note': 'Note',
        'tab': 'Tab',
        'all_dialogs': 'All Dialogs',
        'new_dialog': 'New Dialog',
        'delete': 'Delete',
        'save': 'Save',
        'solveit_lite': 'solveit-lite',
        'footer': 'Crafted with FastHTML',
        'restore_code': 'Restore Code',
        'restore_failed': '‼ Restore Failed',
        'restore_error': 'No account found with this restore code. Please check and try again.',
        'enter_restore_code': 'Enter restore code...',
    }
}

:::

::: {#f66dc855 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:32.453817+00:00’}

def t(key, session=None): 
    lang = session.get('lang', 'zh') if session else current_lang.get()
    return T[lang].get(key, key)

:::

t('ask_model', {'lang':'zh'})
'问问模型···'

Chat Bubbles

Bubbles that display the dialog content. Each bubble type has its own color scheme—blue for code, red for prompts, and green for notes.

::: {#4a5b2004 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:34.821086+00:00’}

def Bubble(text, color, type='q', marked=True):
    colors = {
        'code': 'border-blue-500 bg-blue-50',
        'note': 'border-green-500 bg-green-50',
        'prompt': 'border-red-500 bg-red-50'
    }
    cls = f"{'marked' if marked else ''} {'ml-6' if type=='r' else ''} flex w-max max-w-[75%] flex-col gap-2 rounded-lg px-3 py-2 text-sm border-2 {colors[color]}"
    return Div(text, cls=cls)

:::

prev(Bubble('**嗨喽**', 'note'))
prev(Div(
    Bubble('嗨!', 'prompt'),
    Bubble('哈喽!', 'prompt', 'r')
))
prev(Bubble('''
```py
print('嗨!!!')
''', 'code'))

Component Wrappers

::: {#06830b37 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:37.358269+00:00’}

import fasthtml.components as fc

:::

::: {#0db9e155 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:37.810210+00:00’}

def mk_comp(name, def_cls):
    comp = getattr(fc, name)
    globals()[name] = lambda *args, var='', cls='', **kwargs: comp(*args, cls=f'{def_cls}{"-"+var if var else ""} {cls}', **kwargs)

:::

This wrapper will allow us to define HTML components with Basecoat styling without redundantly stating what that component is.

Before:

prev(Button('🔘', cls='btn'))
prev(Button('🔘', cls='btn-destructive'))

::: {#2e0f1420 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:20:40.220043+00:00’}

mk_comp('Button', 'btn')
mk_comp('Input', 'input')
mk_comp('Textarea', 'textarea')
mk_comp('Label', 'label')

:::

After:

prev(Button('🔘'))
prev(Button('🔘', var='destructive'))
to_xml(Button('🔘')), to_xml(Button('🔘', var='destructive'))
('<button class="btn ">🔘</button>',
 '<button class="btn-destructive ">🔘</button>')

Input Box

The input area that will be used for entering messages to the dialog.

::: {#ae807178 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:22:41.794275+00:00’}

def Inp(type='text', placeholder='输入内容···', id='inp', oob=False, **kwargs): 
    return Textarea(type='text', rows=1, placeholder=placeholder, id=id, cls='font-mono', **(dict(hx_swap_oob='true') if oob else {}), **kwargs)

:::

prev(Inp())
to_xml(Inp())
'<textarea type="text" rows="1" placeholder="输入内容···" id="inp" class="textarea font-mono" name="inp"></textarea>'

Button

prev(Button('提交', type='submit'))

Route Helper Functions

Helper functions for managing chat sessions and rendering responses. get_chat retrieves the active chat and shell for the current user’s dialog. sync_hist persists the conversation history to the database. mk_reply generates the UI response—a pair of bubbles (user input and model output) along with an OOB swap to clear the input field.

user_chats
{(1, 1): (<lisette.core.Chat at 0x7abdd55438f0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5351730>),
 (1, 2): (<lisette.core.Chat at 0x7abdd52451c0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5245100>)}
fake_session = {'userid':1, 'current_dialog':1}

::: {#7eba2cc7 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:22:47.296296+00:00’}

def get_chat(session): return user_chats[(session['userid'], session['current_dialog'])]

:::

get_chat(fake_session)
(<lisette.core.Chat at 0x7abdd55438f0>,
 <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5351730>)

::: {#649b261d .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:22:48.448754+00:00’}

def sync_hist(session, hist): dialogs.update(uid=session['userid'], dialog_num=session['current_dialog'], messages=json.dumps([h.model_dump() if hasattr(h, 'model_dump') else h for h in hist]))

:::

c = Chat('moonshot/kimi-k2-0711-preview', sp=sp)
c('你是谁?'); c.hist
[{'role': 'user', 'content': '你是谁?'},
 Message(content='我是Kimi,由月之暗面科技有限公司训练的大语言模型。很高兴为你提供帮助!', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'refusal': None})]
sync_hist(fake_session, c.hist)
json.loads(dialogs[(1, 1)].messages)
[{'role': 'user', 'content': '你是谁?'},
 {'content': '我是Kimi,由月之暗面科技有限公司训练的大语言模型。很高兴为你提供帮助!',
  'role': 'assistant',
  'tool_calls': None,
  'function_call': None,
  'provider_specific_fields': {'refusal': None}}]

::: {#c862e101 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:29:51.580699+00:00’}

def mk_reply(inp, out, color, name='inp', placeholder='输入内容···'): return Inp(oob=True, name=name, placeholder=placeholder, onkeydown="if(event.shiftKey && event.key==='Enter') { event.preventDefault(); this.form.requestSubmit(); }"), Div(cls='space-y-0.5')(
    Bubble(inp, color), 
    Bubble(out, color, 'r') if out else None)

:::

prev(mk_reply('呵呵', '哈哈', 'prompt'))

LLM Route

r = c('嗨'); r.choices[0].message.content
'嗨!有什么我可以帮你的吗?'

::: {#744b0ef0 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:04.763477+00:00’}

def add_hist(hist, type, content='', role='user'):
    hist.append(dict(role=role, content=content, type=type))

:::

I’ve added a type key to allow me to know in what style to render the message (blue for 'code', red for 'prompt', and green for 'note') when I load them.

add_hist(c.hist, 'note', content='这只是一张便条')
c.hist
[{'role': 'user', 'content': '你是谁?'},
 Message(content='我是Kimi,由月之暗面科技有限公司训练的大语言模型。很高兴为你提供帮助!', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'refusal': None}),
 {'role': 'user', 'content': '嗨'},
 Message(content='嗨!有什么我可以帮你的吗?', role='assistant', tool_calls=None, function_call=None, provider_specific_fields={'refusal': None}),
 {'role': 'user', 'content': '这只是一张便条', 'type': 'note'}]

::: {#e8fb36a9 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:04.775746+00:00’}

import json
@rt
def ask_llm(qry:str, session):
    c,sh = get_chat(session)
    res = c(qry)
    content = res.choices[0].message.content
    c.hist[-2]['type'] = 'prompt'
    c.hist[-1]['type'] = 'prompt'
    sync_hist(session, c.hist)
    return mk_reply(qry, content, 'prompt', 'qry', t('ask_model'))

:::

ask_llm, alongside quering the model, stores the conversation with a type marker for rendering and syncs the history to the database.

prev('/ask_llm?qry=你好')
def test(inp):
    return Div(Div(id='msgs'),
    Form(hx_post=ask_llm, hx_target='#msgs', hx_swap='beforeend', cls='flex items-center space-x-2')(inp, Button('提交', type='submit')))
prev(test(Inp(id='qry')))
user_chats
{(1, 1): (<lisette.core.Chat at 0x7abdd55438f0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5351730>),
 (1, 2): (<lisette.core.Chat at 0x7abdd52451c0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5245100>)}

Code Route

sh = get_shell(); sh
<IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd40e5bb0>
??get_shell
def get_shell()->TerminalInteractiveShell:
    "Get a `TerminalInteractiveShell` with minimal functionality"
    sh = TerminalInteractiveShell()
    sh.logger.log_output = sh.history_manager.enabled = False
    dh = sh.displayhook
    dh.finish_displayhook = dh.write_output_prompt = dh.start_displayhook = lambda: None
    dh.write_format_data = lambda format_dict, md_dict=None: None
    sh.logstart = sh.automagic = sh.autoindent = False
    sh.autocall = 0
    sh.system = lambda cmd: None
    return sh

File: /usr/local/lib/python3.12/site-packages/toolslm/shell.py

get_shell relies on IPython under the hood.

::: {#0f20abdc .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:11.164968+00:00’}

def ex(code, sh):
    res = sh.run_cell(code)
    return res.result if res.result else res.stdout

:::

::: {#3cd307bc .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:11.722335+00:00’}

@rt
def ex_code(code:str, session):
    c,sh = get_chat(session)
    add_hist(c.hist, 'code', f'[INTERPRETER INPUT]\n```py\n{code}\n```')
    res = ex(code, sh)
    add_hist(c.hist, 'code', f'[INTERPRETER OUTPUT]\n```py\n{res}\n```', role='assistant')
    sync_hist(session, c.hist)
    return mk_reply(inp=f'```py\n{code}\n```', out= f'```py\n{res}\n```', color='code', name='code', placeholder=t('your_code'))

:::

ex_code similarly also stores the conversation with a type marker and syncs with the database. A special '[INTERPRETER INPUT/OUTPUT]' marker is also added as indicators for the LLM.

prev(Div(
    Div(id='msgs'),
    Form(hx_post=ex_code, hx_target='#msgs', hx_swap='beforeend', cls='flex items-center space-x-2')(Inp(name='code'), Button('提交', type='submit'))
))

Note Route

::: {#a591691c .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:14.029374+00:00’}

@rt
def add_note(note:str, session):
    c,sh = get_chat(session)
    add_hist(c.hist, 'note', content=note,)
    sync_hist(session, c.hist)
    return mk_reply(note, '', 'note', 'note', t('any_thoughts'))

:::

def test(inp):
    return Div(Div(id='msgs'),
    Form(hx_post=add_note, hx_target='#msgs', hx_swap='beforeend', cls='flex items-center space-x-2')(inp, Button('提交', type='submit')))
prev(test(Inp(id='note')))

Editor Component

Three editor components for the different message types. Each editor is a form that posts to its corresponding route (ex_code, ask_llm, or add_note) and appends the result to the message area.

::: {#3da6f419 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:16.645443+00:00’}

@rt
def code_editor(session): return Form(hx_post=ex_code, hx_target='#msgs', hx_swap='beforeend', cls='flex items-stretch space-x-2')(Inp(placeholder=t('enter_code', session), name='code', onkeydown="if(event.shiftKey && event.key==='Enter') { event.preventDefault(); this.form.requestSubmit(); }"), Button(t('submit', session), type='submit', cls='self-stretch h-auto'))

:::

The editor routes need the session cookie for translations because each editor is loaded in a separate request. Since ContextVar resets with each new request, we need to re-set the language from the session at the start of each request.

prev(Div(Div(id='msgs'),Div(hx_get=code_editor, hx_trigger='load')))

::: {#e85cd35e .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:17.898128+00:00’}

@rt
def llm_editor(session): return Form(hx_post=ask_llm, hx_target='#msgs', hx_swap='beforeend', cls='flex items-center space-x-2')(Inp(placeholder=t('ask_model', session), name='qry', onkeydown="if(event.shiftKey && event.key==='Enter') { event.preventDefault(); this.form.requestSubmit(); }"), Button(t('submit', session), type='submit', cls='self-stretch h-auto'))

:::

prev(llm_editor)

::: {#3f0d14d8 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:19.180056+00:00’}

@rt
def note_editor(session): return Form(hx_post=add_note, hx_target='#msgs', hx_swap='beforeend', cls='flex items-center space-x-2')(Inp(placeholder=t('any_thoughts', session), name='note', onkeydown="if(event.shiftKey && event.key==='Enter') { event.preventDefault(); this.form.requestSubmit(); }"), Button(t('submit', session), type='submit', cls='self-stretch h-auto'))

:::

prev(note_editor)

Tabs

A tabbed navigation bar that lets users switch between the three editor types—code, prompt, and note. Each tab triggers an HTMX request to load the corresponding editor form into the editor container.

::: {#e2b96dec .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:21.370973+00:00’}

def Tab(title, id, hx_post, **kwargs): return fc.Button(title, id=f'{id}-tab', type='button', role='tab', hx_post=hx_post, hx_target='#editor', hx_swap='innerHTML', **kwargs)

:::

prev(Nav(Tab('标签', id='test', hx_post=None), cls='w-full', role='tablist'))

::: {#355bf665 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:22.177099+00:00’}

def TabBar(cls='', *args, **kwargs): 
    return Nav(
        Tab(t('code'), id='code', hx_post=code_editor),
        Tab(t('prompt'), id='qry', hx_post=llm_editor),
        Tab(t('note'), id='note', hx_post=note_editor),
        cls=f'w-full {cls}', role='tablist', *args, **kwargs
    )

:::

prev(Div(TabBar(), Div(id='editor'), cls='tabs w-full'))

Render Existing Messages

::: {#764bf26a .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:23.989941+00:00’}

@rt
def load_messages(session):
    c,sh = get_chat(session)
    for i in c.hist:
        role = i.get('role') if isinstance(i, dict) else i.role
        if role=='user':      yield Div(Bubble(i['content'].replace('[INTERPRETER INPUT]\n', ''), i['type']), cls='space-y-0.5')
        if role=='assistant': yield Div(Bubble(i['content'].replace('[INTERPRETER OUTPUT]\n', ''), i['type'], 'r'), cls='space-y-0.5')

:::

prev(f'/load_messages')

Dialog

The dialog component ties everything together—a title bar that supports inline editing, a message display area that loads existing conversation history, and the tabbed editor for adding new messages. Clicking a dialog name makes it editable, and saving updates the database immediately.

::: {#64ce33d2 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:26.850900+00:00’}

@rt
def edit_dname(session):
    dname = dialogs[(session['userid'], session['current_dialog'])].name
    return Form(id='dname', hx_post=save_dname, hx_target='#dname', hx_swap='outerHTML')(
        Input(value=dname, name='newname', autofocus=True, onfocus='this.select()', required=True, cls='text-2xl font-bold outline-none'),
        Button(t('save', session), type='submit')
    )

:::

::: {#9d6ae097 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:27.433370+00:00’}

@rt 
def save_dname(session, newname:str):
    dialogs.update(uid=session['userid'], dialog_num=session['current_dialog'], name=newname)
    return DialogTitle(newname)

:::

::: {#dfb739b4 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:27.997019+00:00’}

def DialogTitle(name): return H1(name, cls='text-2xl font-bold', id='dname', hx_get=edit_dname, hx_target='#dname', hx_swap='outerHTML')

:::

prev(DialogTitle('对话'))

::: {#5f7ef951 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:29.271810+00:00’}

@rt
def dialog(dnum:int, session): 
    session['current_dialog'] = dnum
    return Div(
        DialogTitle(dialogs[(session['userid'], dnum)].name),
        Div(id='msgs', cls='p-8 max-h-96 overflow-y-auto', hx_get=load_messages, hx_trigger='load'),
        TabBar(id='tabbar'),
        Div(id='editor'),
        cls='tabs w-full'
    )

:::

prev('dialog?dnum=1')

Delete Dialog

::: {#493dcf03 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:31.698732+00:00’}

@rt
def delete_dialog(session, dnum:int):
    uid = session.get('userid')
    dialogs.delete((uid, dnum))
    del user_chats[(uid, dnum)]

:::

fake_session
{'userid': 1, 'current_dialog': 1}
users(), dialogs()
([User(id=1, restore_code='sparkling-ochre-copperhead')],
 [Dialog(id=None, uid=1, dialog_num=1, name='fine-beetle', messages='[{"role": "user", "content": "\\u4f60\\u662f\\u8c01\\uff1f"}, {"content": "\\u6211\\u662fKimi\\uff0c\\u7531\\u6708\\u4e4b\\u6697\\u9762\\u79d1\\u6280\\u6709\\u9650\\u516c\\u53f8\\u8bad\\u7ec3\\u7684\\u5927\\u8bed\\u8a00\\u6a21\\u578b\\u3002\\u5f88\\u9ad8\\u5174\\u4e3a\\u4f60\\u63d0\\u4f9b\\u5e2e\\u52a9\\uff01", "role": "assistant", "tool_calls": null, "function_call": null, "provider_specific_fields": {"refusal": null}}]', server_session=None),
  Dialog(id=None, uid=1, dialog_num=2, name='fat-junglefowl', messages='[{"role": "user", "content": "\\u4f60\\u597d", "type": "prompt"}, {"content": "\\u4f60\\u597d\\uff01\\u6709\\u4ec0\\u4e48\\u6211\\u53ef\\u4ee5\\u5e2e\\u4f60\\u7684\\u5417\\uff1f", "role": "assistant", "tool_calls": null, "function_call": null, "provider_specific_fields": {"refusal": null}, "type": "prompt"}]', server_session=None)])
user_chats
{(1, 1): (<lisette.core.Chat at 0x7abdd55438f0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5351730>),
 (1, 2): (<lisette.core.Chat at 0x7abdd52451c0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5245100>)}
delete_dialog(fake_session, 1)
users(), dialogs()
([User(id=1, restore_code='sparkling-ochre-copperhead')],
 [Dialog(id=None, uid=1, dialog_num=2, name='fat-junglefowl', messages='[{"role": "user", "content": "\\u4f60\\u597d", "type": "prompt"}, {"content": "\\u4f60\\u597d\\uff01\\u6709\\u4ec0\\u4e48\\u6211\\u53ef\\u4ee5\\u5e2e\\u4f60\\u7684\\u5417\\uff1f", "role": "assistant", "tool_calls": null, "function_call": null, "provider_specific_fields": {"refusal": null}, "type": "prompt"}]', server_session=None)])
user_chats
{(1, 2): (<lisette.core.Chat at 0x7abdd52451c0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5245100>)}

Restore Dialog

The restore session feature allows users to recover their data if the server restarts or they switch devices. Users enter their unique restore code (generated at account creation), which looks up their account in the database. If found, it restores their session by setting the user ID, loading their first dialog, and redirecting to the main page. A destructive alert displays if the restore code is invalid.

::: {#b7ebe597 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T02:05:32.952359+00:00’}

insolveit = 'IN_SOLVEIT' in os.environ

:::

insolveit
True

I’ve created the insolveit check to determine whether the app is running in my SolveIt instance or whether as a deployed app.

::: {#7c59ce58 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T02:06:01.512870+00:00’}

@rt
def restore_session(restore_code:str, session):
    if (u:=first(users('restore_code=?', [restore_code]))):
        session['userid'] = u.id
        existing = dialogs(where=f'uid={u.id}')
        if existing: session['current_dialog'] = existing[0].dialog_num
        setsession(session)
        return HtmxResponseHeaders(redirect='/main' if insolveit else '/')
    else: return Div(cls='alert-destructive')(H2(t('restore_failed')), Section(t('restore_error')))

    session['userid'] = first(users('restore_code=?', [restore_code])).id
    setsession(session)

:::

prev('/restore_session?restore_code=haha')
user_chats
{(1, 2): (<lisette.core.Chat at 0x7abdd52451c0>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd5245100>)}
users[1].restore_code
'sparkling-ochre-copperhead'
user_chats = {}
prev(restore_session.to(restore_code=users[1].restore_code))
user_chats
{(1, 2): (<lisette.core.Chat at 0x7abdc4f84440>,
  <IPython.terminal.interactiveshell.TerminalInteractiveShell at 0x7abdd40dbf20>)}
prev(Div(
    Form(hx_post=restore_session, hx_target='#alert-area', cls='flex items-end gap-2')(
        Div()(
            Label('恢复码', fr='restore_code', cls='mb-2'),
            Input(placeholder='looks-like-this', type='text', name='restore_code')
        ),
        Button('提交', type='submit')
    ),
    Div(id='alert-area')
))

::: {#b997e64f .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:43.679185+00:00’}

def RestoreForm(session, id='restore-form', *args, **kwargs):
    return Form(hx_post=restore_session, hx_target='#alert-box', cls='flex items-end gap-2', id=id, *args, **kwargs)(
        Div(
            Label(t('restore_code'), ' · ', users[session['userid']].restore_code, fr='restore_code', cls='mb-2'),
            Input(type='text', name='restore_code', placeholder=t('enter_restore_code'))
        ),
        Button(t('submit'), type='submit')
    )

:::

prev(RestoreForm(fake_session))

Dialog List

The dialog list component displays all dialogs belonging to the current user. The display_dialogs route fetches all user dialogs from the database and includes a “new dialog” card that triggers dialog creation. The restore code form is also embedded here, allowing users to recover their session from a different device.

::: {#7fde0067 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:46.598862+00:00’}

@rt
def current_dialog(session): return dialog(session.get('current_dialog'), session)

:::

::: {#d1206a9d .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:47.192636+00:00’}

def DialogCard(d): return Div(id=f'dialog-{d.uid}{d.dialog_num}', cls='card p-4 hover:bg-gray-100 flex flex-row justify-between items-center')(
    Span(hx_get=dialog.to(dnum=d.dialog_num), hx_target='#content', cls='cursor-pointer')('💬 ', d.name), 
    Button(t('delete'), var='destructive', hx_post=delete_dialog.to(dnum=d.dialog_num), hx_swap='delete', hx_target=f'#dialog-{d.uid}{d.dialog_num}')
)

:::

prev(DialogCard(first(dialogs())))

::: {#4151e92e .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:48.548452+00:00’}

@rt
def display_dialogs(session):
    ds = L(dialogs('uid=?', [session.get('userid')]))
    ds = ds.map(lambda d: DialogCard(d))
    return Div(cls='flex flex-col gap-2 max-w-xl')(
        Div(cls='flex items-center justify-between gap-4')(H1(t('all_dialogs', session), cls='text-2xl font-bold', id='all-dialogs-title'), RestoreForm(session)), 
        Div(id='alert-box'),
        Div('➕', Span(t('new_dialog', session), id='new-dialog-text'), cls='card p-4 hover:bg-gray-100 cursor-pointer border-2 border-dashed flex flex-row items-center gap-2', hx_post=create_dialog, hx_target='#content'),
        Div(*ds, cls='flex flex-col gap-2 max-h-64 overflow-y-auto')
    )

:::

prev(Div(
    Div(id='content')(
        display_dialogs({'userid': 1, 'current_dialog': 11})
    )
))

Main Page

::: {#886bc7d5 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:23:53.781013+00:00’}

@rt
def toggle_lang(session):
    lang = 'zh' if session.get('lang') == 'en' else 'en'
    current_lang.set(lang)
    session['lang'] = lang
    return (
        H2(t('solveit_lite'), id='title', hx_swap_oob='true'),
        Button(t('all_dialogs'), id='dialogs-btn', hx_post=display_dialogs, hx_target='#content', var='secondary', hx_swap_oob='true'),
        Span(t('code'), hx_swap_oob='innerHTML:#code-tab'),
        Span(t('prompt'), hx_swap_oob='innerHTML:#qry-tab'),
        Span(t('note'), hx_swap_oob='innerHTML:#note-tab'),
        Div(id='editor', hx_swap_oob='true'),
        Footer(t('footer'), id='footer', cls='text-sm text-gray-400', hx_swap_oob='true'),
        RestoreForm(session, hx_swap_oob='true', id='restore-form'),
        H1(t('all_dialogs'), cls='text-2xl font-bold', id='all-dialogs-title', hx_swap_oob='true'),
        Span(t('new_dialog'), id='new-dialog-text', hx_swap_oob='true')
    )

:::

This route allows users to switch between Chinese and English with OOB swaps to update all translatable text across the page simultaneously.

::: {#e98d1f46 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’ time_run=‘2026-01-16T01:24:08.328231+00:00’}

@rt('/main' if insolveit else '/')
def main(session):
    setsession(session)
    return Div(id='body', cls='card p-8 max-w-4xl mx-auto my-4')(
        Header(cls='flex justify-between items-center')(H2(
            t('solveit_lite'), id='title'), 
            Span(
                Button('🌐', var='secondary', hx_post=toggle_lang, hx_swap='none'), 
                Button(t('all_dialogs'), id='dialogs-btn', hx_post=display_dialogs, hx_target='#content', var='secondary')
            )
        ),
        Section(id='content')(
            current_dialog(session)
        ),
        Footer(t('footer'), id='footer', cls='text-sm text-gray-400')
    )

:::

prev('/main')

Deploy

::: {#b05651d9 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}

if not insolveit: serve()

:::

Path('requirements.txt').write_text('''\
python-fasthtml
lisette
coolname
ipython
''')
!cat requirements.txt
python-fasthtml
lisette
coolname
ipython
Path('.plash').write_text('export PLASH_APP_NAME=solveit-lite-unofficial')
!cat .plash
export PLASH_APP_NAME=solveit-lite-unofficial
Path('plash.env').write_text(f'export MOONSHOT_API_KEY={os.environ.get('MOONSHOT_API_KEY')}')
75
import plash_cli as pc
pc.deploy()
'https://solveit-lite-unofficial.pla.sh'
print(pc.logs())
Build Start Time: 2026-01-16 01:30:30.917195+00:00
Step 1/14 : FROM plash:latest
 ---> 8c65e51fa2d2
Step 2/14 : ARG USER_UID=1000
 ---> Using cache
 ---> 781580c8e262
Step 3/14 : ARG USER_GID=1000
 ---> Using cache
 ---> dcf3956b8c50
Step 4/14 : RUN groupadd -g $USER_GID plash && useradd -u $USER_UID -g $USER_GID -m plash
 ---> Using cache
 ---> fe019dc49177
Step 5/14 : ENV PLASH_PRODUCTION=1
 ---> Using cache
 ---> a86e28497fc0
Step 6/14 : COPY .dockerignore setup.sh* ./
 ---> Using cache
 ---> 786f3c3d2c09
Step 7/14 : RUN if [ -f ./setup.sh ]; then echo "Found setup.sh, executing..." && chmod +x ./setup.sh && ./setup.sh; fi
 ---> Using cache
 ---> 3f80b2a299d3
Step 8/14 : COPY .dockerignore requirements.txt* ./
 ---> Using cache
 ---> dea62d9dd70b
Step 9/14 : RUN if [ -f ./requirements.txt ]; then echo "Found requirements.txt, executing..." && uv pip install -r ./requirements.txt --system; fi
 ---> Using cache
 ---> 241057bc8973
Step 10/14 : RUN chown -R plash:plash /app
 ---> Using cache
 ---> eb5b444c975f
Step 11/14 : USER plash
 ---> Using cache
 ---> d2eda950b2e1
Step 12/14 : ENV PATH="/home/plash/.local/bin:$PATH"
 ---> Using cache
 ---> 5631843ecbf7
Step 13/14 : EXPOSE 5001
 ---> Using cache
 ---> b1e69db470b1
Step 14/14 : ENTRYPOINT ["bash", "-c", "if [ -f ./plash.env ]; then . ./plash.env; fi && python main.py"]
 ---> Using cache
 ---> 5ad4e58136f3
Successfully built 5ad4e58136f3
Successfully tagged 9d102cbb-e493-4ce7-9110-09dbf5c01851:latest

Build End Time: 2026-01-16 01:30:31.194696+00:00
print(pc.logs(mode='app'))
INFO:     Will watch for changes in these directories: ['/app']
INFO:     Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
INFO:     Started reloader process [1] using WatchFiles
Link: http://localhost:5001
INFO:     Started server process [14]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
import httpx
print(httpx.get('https://solveit-lite-unofficial.pla.sh').text)
 <!doctype html>
 <html>
   <head>
     <title>FastHTML page</title>
     <link rel="canonical" href="https://solveit-lite-unofficial.pla.sh/">
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/basecoat-css@0.3.6/dist/basecoat.cdn.min.css">
<script src="https://cdn.jsdelivr.net/npm/basecoat-css@0.3.6/dist/js/all.min.js" defer></script><script type="module">import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
proc_htmx('.marked', e => e.innerHTML = marked.parse(e.textContent));</script>     <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/atom-one-dark.css" media="(prefers-color-scheme: dark)">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/atom-one-light.css" media="(prefers-color-scheme: light)">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js"></script><script src="https://cdn.jsdelivr.net/gh/arronhunt/highlightjs-copy/dist/highlightjs-copy.min.js"></script>     <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/arronhunt/highlightjs-copy/dist/highlightjs-copy.min.css">
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/languages/python.min.js"></script><script type="module">
hljs.addPlugin(new CopyButtonPlugin());
hljs.configure({'cssSelector': 'pre code:not([data-highlighted="yes"])'});
htmx.onLoad(hljs.highlightAll);</script>   </head>
   <body>
     <div id="body" class="card p-8 max-w-4xl mx-auto my-4">
       <header class="flex justify-between items-center">
         <h2 id="title">solveit-lite</h2>
<span><button hx-post="/toggle_lang" hx-swap="none" class="btn-secondary ">🌐</button><button hx-post="/display_dialogs" hx-target="#content" id="dialogs-btn" class="btn-secondary " name="dialogs-btn">All Dialogs</button></span>       </header>
       <section id="content">
         <div class="tabs w-full">
           <h1 class="text-2xl font-bold" id="dname" hx-get="/edit_dname" hx-target="#dname" hx-swap="outerHTML">intrepid-turaco</h1>
           <div id="msgs" class="p-8 max-h-96 overflow-y-auto" hx-get="/load_messages" hx-trigger="load"></div>
           <nav class="w-full " role="tablist" id="tabbar">
<button type="button" role="tab" hx-post="/code_editor" hx-swap="innerHTML" hx-target="#editor" id="code-tab" name="code-tab">Code</button><button type="button" role="tab" hx-post="/llm_editor" hx-swap="innerHTML" hx-target="#editor" id="qry-tab" name="qry-tab">Prompt</button><button type="button" role="tab" hx-post="/note_editor" hx-swap="innerHTML" hx-target="#editor" id="note-tab" name="note-tab">Note</button>           </nav>
           <div id="editor"></div>
         </div>
       </section>
       <footer id="footer" class="text-sm text-gray-400">Crafted with FastHTML</footer>
     </div>
   </body>
 </html>

If you have any comments, questions, suggestions, feedback, criticisms, or corrections, please do post them down in the comment section below!

Back to top