File size: 7,339 Bytes
6079f1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
from fasthtml_hf import setup_hf_backup
import os
from fastapi import FastAPI, HTTPException, Request
from sse_starlette.sse import EventSourceResponse
from fastapi.staticfiles import StaticFiles
from openai import AsyncOpenAI
from fasthtml.common import FastHTML, Html, Head, Title, Body, Div, Button, Textarea, Script, Style, P, Favicon
from fasthtml.common import ft_hx
import bleach

from styles import styles
from script import script

# Set the secret key directly from environment variable or default
secret_key = os.getenv('SECRET_KEY')

# Initialize FastHTML with the provided secret key
app = FastHTML(secret_key=secret_key)


# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")

# Setup Hugging Face backup, explicitly setting a writable directory
setup_hf_backup(app)

client = AsyncOpenAI()

# Dictionary to store user conversations by session ID
conversations = {}

# Allow additional HTML tags and attributes for sanitization
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
    "h1", "h2", "h3", "p", "strong", "em", "ul", "ol", "li", "code", "pre", "blockquote"
]
ALLOWED_ATTRIBUTES = bleach.sanitizer.ALLOWED_ATTRIBUTES

# Resolve paths to favicon images
static_dir = os.path.join(os.path.dirname(__file__), "static")
light_icon = os.path.join(static_dir, "favicon-light.ico")
dark_icon = os.path.join(static_dir, "favicon-dark.ico")

# Custom SVG component
def Svg(*c, viewBox=None, **kwargs):
    return ft_hx('svg', *c, viewBox=viewBox, **kwargs)

# Custom Path component for SVG with color
def Path(*c, d=None, fill=None, **kwargs):
    return ft_hx('path', *c, d=d, fill=fill, **kwargs)

# Homepage route
@app.get("/")
def home():
    """Render homepage with FastGPT UI."""
    home_text = """

## FastGPT - A ChatGPT Implementation Using FastHTML

    """
    
    page = Html(
        Head(
            Title('FastGPT'),
            Favicon(light_icon="/static/favicon-light.ico", dark_icon="/static/favicon-dark.ico"),  # Serve static favicon files
            Style(styles),
            Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"),
            Script(src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.9/purify.min.js")
        ),
        Body(
            Div(
                Div("FastGPT", _class="logo-text"),
                Div(
                    Button(
                        Svg(
                            Path(
                                d="M441 58.9L453.1 71c9.4 9.4 9.4 24.6 0 33.9L424 134.1 377.9 88 407 58.9c9.4-9.4 24.6-9.4 33.9 0zM209.8 256.2L344 121.9 390.1 168 255.8 302.2c-2.9 2.9-6.5 5-10.4 6.1l-58.5 16.7 16.7-58.5c1.1-3.9 3.2-7.5 6.1-10.4zM373.1 25L175.8 222.2c-8.7 8.7-15 19.4-18.3 31.1l-28.6 100c-2.4 8.4-.1 17.4 6.1 23.6s15.2 8.5 23.6 6.1l100-28.6c11.8-3.4 22.5-9.7 31.1-18.3L487 138.9c28.1-28.1 28.1-73.7 0-101.8L474.9 25C446.8-3.1 401.2-3.1 373.1 25zM88 64C39.4 64 0 103.4 0 152L0 424c0 48.6 39.4 88 88 88l272 0c48.6 0 88-39.4 88-88l0-112c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 112c0 22.1-17.9 40-40 40L88 464c-22.1 0-40-17.9-40-40l0-272c0-22.1 17.9-40 40-40l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24L88 64z",
                                fill="#b4b4b4"
                            ),
                            viewBox="0 0 512 512",
                            _class="refresh-icon"
                        ),
                        onclick="location.reload()",
                        _class="refresh-button"
                    ),
                    _class='refresh-container'
                ),
                _class="header"
            ),
            Div(
                Div(
                    Div(id="home-text-container", _class="markdown-container", **{"data-home-text": home_text}),
                    _class='title-wrapper'
                ),
                P(id='output'),
                Div(
                    Textarea(
                        id='message',
                        rows=1,
                        cols=50,
                        placeholder="Message FastGPT",
                        oninput="autoResizeTextarea()",
                        onkeypress="checkEnter(event)"
                    ),
                    Button(
                        Svg(
                            Path(
                                d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2 160 448c0 17.7 14.3 32 32 32s32-14.3 32-32l0-306.7L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"
                            ),
                            viewBox="0 0 384 512",
                            _class="send-icon"
                        ),
                        onclick="sendMessage()",
                        _class="send-button"
                    ),
                    _class="container"
                ),
                _class="wrapper"
            ),
            Script(script)
        )
    )
    return page

# Route to stream responses based on user input
@app.get("/stream")
async def stream_response(request: Request, message: str, session_id: str = None):
    """Stream responses for the given user input."""
    if not message:
        raise HTTPException(status_code=400, detail="Message parameter is required")
    if not session_id:
        raise HTTPException(status_code=400, detail="Session ID is required")

    # Initialize conversation if the session ID is new
    if session_id not in conversations:
        conversations[session_id] = [
            {"role": "system", "content": "You are a helpful assistant. Use Markdown for formatting."}
        ]

    # Add user's message to the conversation
    conversations[session_id].append({"role": "user", "content": message})

    async def event_generator():
        try:
            # Call OpenAI API to stream response
            response = await client.chat.completions.create(
                model="gpt-4o-mini",
                messages=conversations[session_id],
                stream=True
            )

            assistant_response = ""

            # Stream response chunks to the client
            async for chunk in response:
                if await request.is_disconnected():
                    print(f"Client for session {session_id} disconnected")
                    break

                content = chunk.choices[0].delta.content
                if content:
                    assistant_response += content
                    yield {"data": content}

            # Save assistant's response to the conversation
            conversations[session_id].append({"role": "assistant", "content": assistant_response})

        except Exception as e:
            yield {"data": f"Error: {str(e)}"}

        finally:
            print(f"Streaming finished for session {session_id}")

    return EventSourceResponse(event_generator())

# Route to reset conversation for a given session ID
@app.get("/reset")
def reset_conversation(session_id: str):
    """Reset the conversation for the specified session ID."""
    if session_id in conversations:
        del conversations[session_id]
    return {"message": "Conversation reset."}