Skip to content

Commit 2ad2016

Browse files
authored
Create app.py
1 parent e8904bc commit 2ad2016

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed

app.py

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import streamlit as st
2+
from openai import OpenAI
3+
import httpx
4+
import requests
5+
import re
6+
import json
7+
import os
8+
import pyperclip
9+
10+
# --- Helper to fetch model list ---
11+
def fetch_models(base_url: str, api_key: str):
12+
try:
13+
if base_url.startswith("http://localhost:11434") or re.match(r"^http://[a-zA-Z0-9.-]+:11434$", base_url):
14+
res = requests.get(base_url.rstrip("/") + "/api/tags")
15+
res.raise_for_status()
16+
data = res.json()
17+
return sorted([m["name"] for m in data.get("models", [])])
18+
else:
19+
endpoint = base_url.rstrip("/") + "/models"
20+
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
21+
r = httpx.get(endpoint, headers=headers, timeout=10)
22+
r.raise_for_status()
23+
data = r.json()
24+
models = [m["id"] for m in data.get("data", []) if m.get("id")]
25+
return sorted(models)
26+
except Exception as e:
27+
st.error(f"Could not fetch models: {e}")
28+
return []
29+
30+
# --- Save chat history to JSON ---
31+
def save_chat_history(messages, filename="chat_history.json"):
32+
try:
33+
with open(filename, "w", encoding="utf-8") as f:
34+
json.dump(messages, f, indent=2, ensure_ascii=False)
35+
except Exception as e:
36+
st.error(f"Error saving chat history: {e}")
37+
38+
# --- Load from JSON ---
39+
def load_chat_history(filename="chat_history.json"):
40+
try:
41+
if os.path.exists(filename):
42+
with open(filename, "r", encoding="utf-8") as f:
43+
return json.load(f)
44+
except Exception as e:
45+
st.error(f"Error loading chat history: {e}")
46+
return []
47+
48+
# --- Clipboard copy helper ---
49+
def copy_to_clipboard(text):
50+
try:
51+
pyperclip.copy(text)
52+
st.toast("Copied to clipboard!")
53+
except Exception as e:
54+
st.error(f"Could not copy to clipboard: {e}")
55+
56+
# --- Text truncation helper ---
57+
def get_truncated_text(text, word_limit=50):
58+
words = text.split()
59+
if len(words) > word_limit:
60+
return ' '.join(words[:word_limit]) + "..."
61+
return text
62+
63+
# --- Page config ---
64+
st.set_page_config(page_title="Custom LLM Chat", page_icon="💬")
65+
st.title("💬 Chat with Any Model Provider")
66+
67+
# --- Sidebar: credentials & model list ---
68+
with st.sidebar:
69+
st.header("Connect")
70+
base_url = st.text_input("Base URL (no trailing slash) & Hit Enter", value=st.session_state.get("base_url", ""))
71+
api_key_required = not re.match(r"^http://[a-zA-Z0-9.-]+:11434$", base_url)
72+
api_key = st.text_input("API Key", type="password", value=st.session_state.get("api_key", "")) if api_key_required else ""
73+
74+
if st.button("List / Refresh Models", use_container_width=True):
75+
if base_url:
76+
with st.spinner("Fetching models…"):
77+
model_list = fetch_models(base_url, api_key)
78+
st.session_state["model_list"] = model_list
79+
st.session_state["base_url"] = base_url
80+
st.session_state["api_key"] = api_key
81+
else:
82+
st.error("Please enter Base URL")
83+
84+
if "model_list" in st.session_state:
85+
chosen_model = st.selectbox(
86+
"Select a model:",
87+
st.session_state["model_list"],
88+
key="chosen_model",
89+
)
90+
else:
91+
chosen_model = None
92+
93+
# --- Chat area ---
94+
if chosen_model and "base_url" in st.session_state:
95+
if "messages" not in st.session_state:
96+
st.session_state.messages = load_chat_history()
97+
98+
base_url = st.session_state["base_url"].rstrip("/")
99+
if not base_url.endswith("/v1"):
100+
base_url += "/v1"
101+
102+
client = OpenAI(
103+
base_url=base_url,
104+
api_key=st.session_state.get("api_key", ""),
105+
)
106+
107+
st.subheader("Chat History")
108+
109+
i = 0
110+
while i < len(st.session_state.messages):
111+
msg = st.session_state.messages[i]
112+
if msg["role"] == "user":
113+
with st.chat_message("user"):
114+
st.markdown(msg["content"])
115+
i += 1
116+
if i < len(st.session_state.messages) and st.session_state.messages[i]["role"] == "assistant":
117+
assistant_idx = i
118+
response_text = st.session_state.messages[assistant_idx]["content"]
119+
toggle_key = f"read_more_{assistant_idx}"
120+
if toggle_key not in st.session_state:
121+
st.session_state[toggle_key] = False
122+
123+
with st.chat_message("assistant"):
124+
words = response_text.split()
125+
if len(words) > 50 and not st.session_state[toggle_key]:
126+
st.markdown(get_truncated_text(response_text, 50))
127+
else:
128+
st.markdown(response_text)
129+
130+
col1, col2, col3 = st.columns(3)
131+
with col1:
132+
if len(words) > 50:
133+
label = "Read More" if not st.session_state[toggle_key] else "Show Less"
134+
if st.button(label, key=f"toggle_{assistant_idx}"):
135+
st.session_state[toggle_key] = not st.session_state[toggle_key]
136+
st.rerun()
137+
with col2:
138+
if st.button("Copy Output", key=f"copy_{assistant_idx}"):
139+
copy_to_clipboard(response_text)
140+
with col3:
141+
if st.button("🗑️ Delete Response", key=f"delete_{assistant_idx}"):
142+
st.session_state.messages.pop(assistant_idx) # assistant
143+
st.session_state.messages.pop(assistant_idx - 1) # user
144+
save_chat_history(st.session_state.messages)
145+
st.rerun()
146+
i += 1
147+
else:
148+
with st.chat_message("assistant"):
149+
st.markdown(msg["content"])
150+
i += 1
151+
152+
# --- Input box for new message ---
153+
if prompt := st.chat_input("Type your message…"):
154+
st.session_state.messages.append({"role": "user", "content": prompt})
155+
with st.chat_message("user"):
156+
st.markdown(prompt)
157+
158+
st.session_state.messages.append({"role": "assistant", "content": ""})
159+
current_assistant_message_idx = len(st.session_state.messages) - 1
160+
161+
with st.chat_message("assistant"):
162+
stop_button_placeholder = st.empty()
163+
response_placeholder = st.empty()
164+
165+
collected_text = ""
166+
stop_flag_key = f"stop_flag_{current_assistant_message_idx}"
167+
st.session_state[stop_flag_key] = False
168+
st.session_state["is_streaming"] = True
169+
170+
def stop_stream_callback():
171+
st.session_state[stop_flag_key] = True
172+
st.session_state["is_streaming"] = False
173+
174+
stop_button_placeholder.button("Stop Response", on_click=stop_stream_callback, key=f"stop_button_{current_assistant_message_idx}")
175+
176+
try:
177+
stream = client.chat.completions.create(
178+
model=chosen_model,
179+
messages=[
180+
{"role": m["role"], "content": m["content"]}
181+
for m in st.session_state.messages[:-1]
182+
] + [{"role": "user", "content": prompt}],
183+
max_tokens=20000,
184+
stream=True,
185+
)
186+
for chunk in stream:
187+
if st.session_state[stop_flag_key]:
188+
break
189+
delta = chunk.choices[0].delta.content or ""
190+
collected_text += delta
191+
response_placeholder.markdown(collected_text + "▌")
192+
st.session_state.messages[current_assistant_message_idx]["content"] = collected_text
193+
194+
except Exception as e:
195+
st.error(f"Request failed: {e}")
196+
collected_text = st.session_state.messages[current_assistant_message_idx]["content"] + f"\n\n<Request failed: {e}>"
197+
st.session_state.messages[current_assistant_message_idx]["content"] = collected_text
198+
finally:
199+
response_placeholder.markdown(collected_text)
200+
stop_button_placeholder.empty()
201+
st.session_state["is_streaming"] = False
202+
st.session_state.messages[current_assistant_message_idx]["content"] = collected_text
203+
save_chat_history(st.session_state.messages)
204+
205+
# Read More block immediately for new response
206+
toggle_key = f"read_more_{current_assistant_message_idx}"
207+
if toggle_key not in st.session_state:
208+
st.session_state[toggle_key] = False
209+
words = collected_text.split()
210+
if len(words) > 50 and not st.session_state[toggle_key]:
211+
st.markdown(get_truncated_text(collected_text, 50))
212+
else:
213+
st.markdown(collected_text)
214+
215+
col1, col2, col3 = st.columns(3)
216+
with col1:
217+
if len(words) > 50:
218+
label = "Read More" if not st.session_state[toggle_key] else "Show Less"
219+
if st.button(label, key=f"toggle_{current_assistant_message_idx}"):
220+
st.session_state[toggle_key] = not st.session_state[toggle_key]
221+
st.rerun()
222+
with col2:
223+
if st.button("Copy Output", key=f"copy_{current_assistant_message_idx}_final"):
224+
copy_to_clipboard(collected_text)
225+
with col3:
226+
if st.button("🗑️ Delete Response", key=f"delete_{current_assistant_message_idx}_final"):
227+
st.session_state.messages.pop(current_assistant_message_idx)
228+
st.session_state.messages.pop(current_assistant_message_idx - 1)
229+
save_chat_history(st.session_state.messages)
230+
st.rerun()
231+
232+
elif "model_list" not in st.session_state:
233+
st.info("👈 Use the sidebar to enter your credentials and load available models.")
234+
else:
235+
st.info("Please pick a model from the sidebar.")

0 commit comments

Comments
 (0)