boda commited on
Commit
eed9b52
Β·
1 Parent(s): 519de2c

Initial deployment

Browse files
Files changed (3) hide show
  1. README.md +31 -13
  2. app.py +633 -0
  3. requirements.txt +5 -3
README.md CHANGED
@@ -1,19 +1,37 @@
1
  ---
2
- title: Llm Paper Intro Builder
3
- emoji: πŸš€
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
  pinned: false
11
- short_description: Streamlit template space
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LLM Paper Introduction Builder
3
+ emoji: πŸ“
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: streamlit
7
+ sdk_version: 1.36.0
8
+ app_file: app.py
 
9
  pinned: false
 
10
  ---
11
 
12
+ # LLM Paper Introduction Builder
13
 
14
+ A Streamlit app that helps researchers write polished academic paper introductions using major AI models (OpenAI, Anthropic Claude, Google Gemini, NVIDIA).
15
 
16
+ The UI guides you through three structured academic writing moves:
17
+ - **Move 1** β€” Establishing a Territory (why the topic matters)
18
+ - **Move 2** β€” Establishing a Niche (the gap or problem)
19
+ - **Move 3** β€” Occupying the Niche (your solution and contribution)
20
+
21
+ Answer the research questions, then click **Generate introduction** to produce a journal-ready introduction with your chosen model.
22
+
23
+ ## Supported providers
24
+
25
+ | Provider | Key required |
26
+ |----------|-------------|
27
+ | ChatGPT (OpenAI) | OpenAI API key |
28
+ | Claude (Anthropic) | Anthropic API key |
29
+ | Gemini (Google) | Google AI API key |
30
+ | NVIDIA | NVIDIA API key |
31
+
32
+ ## Run locally
33
+
34
+ ```bash
35
+ pip install -r requirements.txt
36
+ streamlit run app.py
37
+ ```
app.py ADDED
@@ -0,0 +1,633 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import textwrap
3
+
4
+ import requests
5
+ import streamlit as st
6
+
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Data – questions organised by Move
10
+ # ---------------------------------------------------------------------------
11
+
12
+ MOVES: dict[str, list[str]] = {
13
+ "Move 1 – Establishing a Territory": [
14
+ "Why is this topic worth investigating?",
15
+ "Who has worked on this topic before? (Review a few key researchers if possible)",
16
+ "What do we already know about this topic?",
17
+ "What have important studies found so far?",
18
+ "What are the real-world or practical implications of this topic?",
19
+ "Why should researchers or society care about this topic?",
20
+ "In which field or context is this topic relevant?",
21
+ "Can this topic be studied using existing methods? If yes, which ones?",
22
+ ],
23
+ "Move 2 – Establishing a Niche": [
24
+ "What problem or gap did you find?",
25
+ "Why is this problem important for your field?",
26
+ "Are there weaknesses or limitations in existing studies? If yes, explain.",
27
+ "Are you extending previous research? How?",
28
+ "Are you looking at the problem from a new perspective?",
29
+ "Has any researcher suggested this problem needs more study?",
30
+ "Is this gap part of a larger unresolved issue?",
31
+ "Are there unclear or confusing findings in the literature?",
32
+ "Are previous studies inconsistent with each other? How?",
33
+ ],
34
+ "Move 3 – Occupying the Niche": [
35
+ "What is the main goal of your study?",
36
+ "What are your research questions?",
37
+ "What type of study is this (qualitative, quantitative, mixed)? Why?",
38
+ "Do you have a hypothesis? (if applicable)",
39
+ "Are you using a new method? If yes, explain briefly why it is needed.",
40
+ "Are you proposing a new idea or theory? If yes, explain briefly why it is needed.",
41
+ "What are the expected contributions of your study?",
42
+ "Who will benefit from your research? How?",
43
+ "How does your study address the identified problem?",
44
+ "How is your approach better than existing ones?",
45
+ "What could be the weaknesses of your solution and how do you overcome them?",
46
+ "What is the novelty of your research and why should we accept that?",
47
+ ],
48
+ }
49
+
50
+ MOVE_SUBTITLES = {
51
+ "Move 1 – Establishing a Territory": "Why this topic matters",
52
+ "Move 2 – Establishing a Niche": "What is missing, wrong, or unclear",
53
+ "Move 3 – Occupying the Niche": "Your solution and contribution",
54
+ }
55
+
56
+ MOVE_ICONS = {
57
+ "Move 1 – Establishing a Territory": "🌍",
58
+ "Move 2 – Establishing a Niche": "πŸ”",
59
+ "Move 3 – Occupying the Niche": "πŸš€",
60
+ }
61
+
62
+ MOVE_LABELS = list(MOVES.keys())
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Model catalogues per provider
66
+ # ---------------------------------------------------------------------------
67
+
68
+ OPENAI_MODELS = {
69
+ "GPT-5.5": "gpt-5.5",
70
+ "GPT-5.5 Pro": "gpt-5.5-pro",
71
+ "GPT-5.4": "gpt-5.4",
72
+ "GPT-5.4 Pro": "gpt-5.4-pro",
73
+ "GPT-5.4 mini": "gpt-5.4-mini",
74
+ "GPT-5.4 nano": "gpt-5.4-nano",
75
+ "GPT-5": "gpt-5",
76
+ "GPT-5 mini": "gpt-5-mini",
77
+ "GPT-5 nano": "gpt-5-nano",
78
+ "GPT-4.1": "gpt-4.1",
79
+ "GPT-4.1 mini": "gpt-4.1-mini",
80
+ "o3": "o3",
81
+ "o3 Pro": "o3-pro",
82
+ }
83
+
84
+ CLAUDE_MODELS = {
85
+ "Claude Opus 4.7": "claude-opus-4-7",
86
+ "Claude Sonnet 4.6": "claude-sonnet-4-6",
87
+ "Claude Haiku 4.5": "claude-haiku-4-5-20251001",
88
+ "Claude Opus 4.5": "claude-opus-4-5",
89
+ "Claude Sonnet 3.7": "claude-sonnet-3-7",
90
+ }
91
+
92
+ GEMINI_MODELS = {
93
+ "Gemini 2.5 Flash": "gemini-2.5-flash-preview-05-20",
94
+ "Gemini 2.5 Pro": "gemini-2.5-pro-preview-05-06",
95
+ "Gemini 2.0 Flash": "gemini-2.0-flash",
96
+ "Gemini 1.5 Pro": "gemini-1.5-pro",
97
+ "Gemini 1.5 Flash": "gemini-1.5-flash",
98
+ }
99
+
100
+ NVIDIA_MODELS = {
101
+ "Kimi K2.6 (MoonshotAI)": "moonshotai/kimi-k2.6",
102
+ "Llama 3.1 405B Instruct": "meta/llama-3.1-405b-instruct",
103
+ "Llama 3.3 70B Instruct": "meta/llama-3.3-70b-instruct",
104
+ "Mistral Large 2": "mistralai/mistral-large-2-instruct",
105
+ "Qwen3 235B A22B": "qwen/qwen3-235b-a22b",
106
+ "DeepSeek R1": "deepseek-ai/deepseek-r1",
107
+ }
108
+
109
+
110
+ DEFAULT_INSTRUCTION = textwrap.dedent(
111
+ """
112
+ Write a strong academic paper introduction based on the research notes below.
113
+ Use a formal scholarly tone, move from broad context to the specific study, and do not invent facts, results, or citations.
114
+ The introduction should clearly explain the problem, why it matters, what is known, the gap, the objective, the method, the novelty, and the contribution.
115
+ Keep the writing coherent and polished for a journal-style manuscript.
116
+ """
117
+ ).strip()
118
+
119
+ NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Page config & CSS
123
+ # ---------------------------------------------------------------------------
124
+
125
+ st.set_page_config(
126
+ page_title="LLM Paper Introduction Builder",
127
+ page_icon="πŸ“",
128
+ layout="wide",
129
+ initial_sidebar_state="expanded",
130
+ )
131
+
132
+ st.markdown(
133
+ """
134
+ <style>
135
+ :root {
136
+ --app-bg-top-left: rgba(14, 165, 233, 0.14);
137
+ --app-bg-top-right: rgba(34, 197, 94, 0.12);
138
+ --app-bg-main-start: #f7fbff;
139
+ --app-bg-main-end: #f3f7fb;
140
+ --card-bg: rgba(255, 255, 255, 0.85);
141
+ --card-border: rgba(15, 23, 42, 0.08);
142
+ --hero-title: #0f172a;
143
+ --hero-text: #334155;
144
+ --section-label: #475569;
145
+ --move1: #3b82f6;
146
+ --move2: #f59e0b;
147
+ --move3: #10b981;
148
+ }
149
+ @media (prefers-color-scheme: dark) {
150
+ :root {
151
+ --app-bg-top-left: rgba(56, 189, 248, 0.24);
152
+ --app-bg-top-right: rgba(74, 222, 128, 0.22);
153
+ --app-bg-main-start: #0b1220;
154
+ --app-bg-main-end: #0f172a;
155
+ --card-bg: rgba(15, 23, 42, 0.70);
156
+ --card-border: rgba(148, 163, 184, 0.28);
157
+ --hero-title: #f8fafc;
158
+ --hero-text: #cbd5e1;
159
+ --section-label: #94a3b8;
160
+ }
161
+ }
162
+ .stApp {
163
+ background:
164
+ radial-gradient(circle at top left, var(--app-bg-top-left), transparent 28%),
165
+ radial-gradient(circle at top right, var(--app-bg-top-right), transparent 24%),
166
+ linear-gradient(180deg, var(--app-bg-main-start) 0%, var(--app-bg-main-end) 100%);
167
+ }
168
+
169
+ /* Hero banner */
170
+ .hero { padding:1.4rem 1.5rem; border-radius:1.25rem; background:var(--card-bg);
171
+ backdrop-filter:blur(10px); border:1px solid var(--card-border);
172
+ box-shadow:0 18px 45px rgba(15,23,42,0.08); margin-bottom:1.2rem; }
173
+ .hero h1 { margin:0; font-size:2.1rem; line-height:1.05; color:var(--hero-title); }
174
+ .hero p { margin:0.55rem 0 0; font-size:1rem; color:var(--hero-text); }
175
+
176
+ /* Generic cards */
177
+ .subtle-card { padding:0.9rem 1rem; border-radius:1rem; background:var(--card-bg);
178
+ border:1px solid var(--card-border); box-shadow:0 10px 30px rgba(15,23,42,0.06); }
179
+ .section-label { font-size:0.78rem; font-weight:600; text-transform:uppercase;
180
+ letter-spacing:0.08em; color:var(--section-label); margin-bottom:0.35rem; }
181
+
182
+ /* Move header cards */
183
+ .move-header { display:flex; align-items:flex-start; gap:0.75rem;
184
+ padding:0.85rem 1rem; border-radius:1rem; margin-bottom:0.5rem;
185
+ background:var(--card-bg); border-left:4px solid; border-top:1px solid var(--card-border);
186
+ border-right:1px solid var(--card-border); border-bottom:1px solid var(--card-border); }
187
+ .move-header.m1 { border-left-color: var(--move1); }
188
+ .move-header.m2 { border-left-color: var(--move2); }
189
+ .move-header.m3 { border-left-color: var(--move3); }
190
+ .move-icon { font-size:1.6rem; line-height:1; }
191
+ .move-title { font-size:1rem; font-weight:700; color:var(--hero-title); margin:0; }
192
+ .move-subtitle { font-size:0.82rem; color:var(--section-label); margin:0.1rem 0 0; }
193
+
194
+ /* Answered Q&A cards */
195
+ .qa-item { padding:0.65rem 0.9rem; border-radius:0.75rem; background:var(--card-bg);
196
+ border:1px solid var(--card-border); margin-bottom:0.45rem; }
197
+ .qa-move-badge { display:inline-block; font-size:0.68rem; font-weight:600;
198
+ text-transform:uppercase; letter-spacing:0.06em; padding:0.15rem 0.5rem;
199
+ border-radius:999px; margin-bottom:0.35rem; }
200
+ .badge-m1 { background:rgba(59,130,246,0.15); color:#2563eb; }
201
+ .badge-m2 { background:rgba(245,158,11,0.15); color:#d97706; }
202
+ .badge-m3 { background:rgba(16,185,129,0.15); color:#059669; }
203
+ .qa-q { font-weight:600; font-size:0.9rem; color:var(--hero-title); margin-bottom:0.25rem; }
204
+ .qa-a { font-size:0.88rem; color:var(--hero-text); white-space:pre-wrap; }
205
+ </style>
206
+ """,
207
+ unsafe_allow_html=True,
208
+ )
209
+
210
+ st.markdown(
211
+ """
212
+ <div class="hero">
213
+ <h1>πŸ“ LLM Paper Introduction Builder</h1>
214
+ <p>Answer structured research questions across three academic writing moves, then generate a polished introduction with any major AI model.</p>
215
+ </div>
216
+ """,
217
+ unsafe_allow_html=True,
218
+ )
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Session state init
222
+ # ---------------------------------------------------------------------------
223
+
224
+ if "qa_pairs" not in st.session_state:
225
+ st.session_state["qa_pairs"] = []
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Sidebar – provider & model settings
230
+ # ---------------------------------------------------------------------------
231
+
232
+ PROVIDERS = ["ChatGPT (OpenAI)", "Claude (Anthropic)", "Gemini (Google)", "NVIDIA"]
233
+ PROVIDER_ICONS = {
234
+ "ChatGPT (OpenAI)": "🟒",
235
+ "Claude (Anthropic)": "🟠",
236
+ "Gemini (Google)": "πŸ”΅",
237
+ "NVIDIA": "🟣",
238
+ }
239
+
240
+ with st.sidebar:
241
+ st.markdown('<div class="section-label">Provider</div>', unsafe_allow_html=True)
242
+ backend = st.selectbox(
243
+ "Model provider",
244
+ PROVIDERS,
245
+ format_func=lambda p: f"{PROVIDER_ICONS[p]} {p}",
246
+ )
247
+
248
+ st.markdown("---")
249
+ st.markdown(f'<div class="section-label">{backend} Settings</div>', unsafe_allow_html=True)
250
+
251
+ # -- OpenAI ---------------------------------------------------------------
252
+ if backend == "ChatGPT (OpenAI)":
253
+ openai_api_key = st.text_input("OpenAI API key", type="password", placeholder="sk-...")
254
+ selected_openai_model = st.selectbox("Model", list(OPENAI_MODELS.keys()))
255
+ temperature = st.slider("Temperature", 0.0, 1.5, 0.7, 0.05)
256
+
257
+ # -- Claude ---------------------------------------------------------------
258
+ elif backend == "Claude (Anthropic)":
259
+ claude_api_key = st.text_input("Anthropic API key", type="password", placeholder="sk-ant-...")
260
+ selected_claude_model = st.selectbox("Model", list(CLAUDE_MODELS.keys()))
261
+ temperature = st.slider("Temperature", 0.0, 1.0, 0.7, 0.05)
262
+
263
+ # -- Gemini ---------------------------------------------------------------
264
+ elif backend == "Gemini (Google)":
265
+ gemini_api_key = st.text_input("Google AI API key", type="password", placeholder="AIza...")
266
+ selected_gemini_model = st.selectbox("Model", list(GEMINI_MODELS.keys()))
267
+ temperature = st.slider("Temperature", 0.0, 1.5, 0.7, 0.05)
268
+
269
+ # -- NVIDIA ---------------------------------------------------------------
270
+ elif backend == "NVIDIA":
271
+ nvidia_api_key = st.text_input("NVIDIA API key", type="password", placeholder="nvapi-...")
272
+ selected_nvidia_model = st.selectbox("Model", list(NVIDIA_MODELS.keys()))
273
+ temperature = st.slider("Temperature", 0.0, 1.5, 1.0, 0.05)
274
+ nvidia_thinking = st.checkbox("Enable thinking mode", value=True,
275
+ help="Adds chain-of-thought reasoning (supported by some models).")
276
+
277
+ st.markdown("---")
278
+ st.markdown(
279
+ "<div class='subtle-card' style='font-size:0.82rem;'>Your API key is used only for this "
280
+ "session and is never stored or logged.</div>",
281
+ unsafe_allow_html=True,
282
+ )
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # Generation helpers
287
+ # ---------------------------------------------------------------------------
288
+
289
+
290
+ def build_research_prompt(instruction: str, qa_pairs: list[dict]) -> str:
291
+ note_lines = []
292
+ for i, pair in enumerate(qa_pairs, 1):
293
+ move_short = pair["move"].split("–")[0].strip()
294
+ a = pair["answer"].strip() or "[Not provided]"
295
+ note_lines.append(f"Q{i} ({move_short}): {pair['question']}\nA{i}: {a}")
296
+ notes_block = "\n\n".join(note_lines)
297
+ return textwrap.dedent(
298
+ f"""
299
+ {instruction.strip()}
300
+
301
+ Research notes:
302
+ {notes_block}
303
+
304
+ Task:
305
+ Write a complete paper introduction based only on the information above.
306
+ Do not fabricate results, references, statistics, or claims not supported by the notes.
307
+ If an answer is missing, keep that detail general rather than inventing it.
308
+ Return only the introduction text.
309
+ """
310
+ ).strip()
311
+
312
+
313
+ def _system_messages(provider: str) -> list[dict]:
314
+ return [{"role": "system", "content": "You are a careful academic writing assistant."}]
315
+
316
+
317
+ def generate_openai(api_key: str, model_id: str, prompt: str, temperature: float) -> str:
318
+ from openai import OpenAI
319
+ client = OpenAI(api_key=api_key)
320
+ # GPT-5.x models require max_completion_tokens; older models accept both
321
+ tokens_kwarg = "max_completion_tokens" if model_id.startswith("gpt-5") else "max_tokens"
322
+ response = client.chat.completions.create(
323
+ model=model_id,
324
+ messages=_system_messages("openai") + [{"role": "user", "content": prompt}],
325
+ temperature=temperature,
326
+ **{tokens_kwarg: 1500},
327
+ )
328
+ return response.choices[0].message.content.strip()
329
+
330
+
331
+ def generate_claude(api_key: str, model_id: str, prompt: str, temperature: float) -> str:
332
+ import anthropic
333
+ client = anthropic.Anthropic(api_key=api_key)
334
+ message = client.messages.create(
335
+ model=model_id,
336
+ max_tokens=1500,
337
+ temperature=temperature,
338
+ system="You are a careful academic writing assistant.",
339
+ messages=[{"role": "user", "content": prompt}],
340
+ )
341
+ return message.content[0].text.strip()
342
+
343
+
344
+ def generate_gemini(api_key: str, model_id: str, prompt: str, temperature: float) -> str:
345
+ from google import genai
346
+ from google.genai import types
347
+ client = genai.Client(api_key=api_key)
348
+ response = client.models.generate_content(
349
+ model=model_id,
350
+ contents=f"You are a careful academic writing assistant.\n\n{prompt}",
351
+ config=types.GenerateContentConfig(
352
+ temperature=temperature,
353
+ max_output_tokens=1500,
354
+ ),
355
+ )
356
+ return response.text.strip()
357
+
358
+
359
+ def generate_nvidia(api_key: str, model_id: str, prompt: str, temperature: float, thinking: bool) -> str:
360
+ messages = [
361
+ {"role": "system", "content": "You are a careful academic writing assistant."},
362
+ {"role": "user", "content": prompt},
363
+ ]
364
+ payload: dict = {
365
+ "model": model_id,
366
+ "messages": messages,
367
+ "max_tokens": 1500,
368
+ "temperature": temperature,
369
+ "top_p": 1.0,
370
+ "stream": True,
371
+ }
372
+ if thinking:
373
+ payload["chat_template_kwargs"] = {"thinking": True}
374
+
375
+ headers = {
376
+ "Authorization": f"Bearer {api_key}",
377
+ "Accept": "text/event-stream",
378
+ }
379
+
380
+ response = requests.post(NVIDIA_API_URL, headers=headers, json=payload, stream=True)
381
+ response.raise_for_status()
382
+
383
+ chunks: list[str] = []
384
+ for raw_line in response.iter_lines():
385
+ if not raw_line:
386
+ continue
387
+ line = raw_line.decode("utf-8")
388
+ if line.startswith("data:"):
389
+ data_str = line[len("data:"):].strip()
390
+ if data_str == "[DONE]":
391
+ break
392
+ try:
393
+ data = json.loads(data_str)
394
+ delta = data["choices"][0].get("delta", {})
395
+ content = delta.get("content") or ""
396
+ chunks.append(content)
397
+ except (json.JSONDecodeError, KeyError, IndexError):
398
+ continue
399
+
400
+ return "".join(chunks).strip()
401
+
402
+
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # Main layout
406
+ # ---------------------------------------------------------------------------
407
+
408
+ left_col, right_col = st.columns([1.4, 0.85])
409
+
410
+ with left_col:
411
+ # ── Instruction prompt ───────────────────────────────────────────────────
412
+ st.markdown('<div class="section-label">Instruction Prompt</div>', unsafe_allow_html=True)
413
+ instruction_prompt = st.text_area(
414
+ "Instruction for the model",
415
+ value=DEFAULT_INSTRUCTION,
416
+ height=140,
417
+ label_visibility="collapsed",
418
+ help="Combined with your answers and sent to the model.",
419
+ )
420
+
421
+ st.markdown("---")
422
+
423
+ # ── Question picker ──────────────────────────────────────────────────────
424
+ st.markdown('<div class="section-label">Research Intake β€” Pick a Question & Answer It</div>', unsafe_allow_html=True)
425
+
426
+ # Move selector displayed as styled headers
427
+ MOVE_CLASSES = ["m1", "m2", "m3"]
428
+ move_cols = st.columns(3)
429
+ for col, (move_label, cls) in zip(move_cols, zip(MOVE_LABELS, MOVE_CLASSES)):
430
+ icon = MOVE_ICONS[move_label]
431
+ subtitle = MOVE_SUBTITLES[move_label]
432
+ title_part = move_label.split("–")[0].strip()
433
+ name_part = move_label.split("–")[1].strip() if "–" in move_label else ""
434
+ col.markdown(
435
+ f'<div class="move-header {cls}">'
436
+ f' <div class="move-icon">{icon}</div>'
437
+ f' <div>'
438
+ f' <p class="move-title">{title_part}</p>'
439
+ f' <p class="move-subtitle">{name_part}</p>'
440
+ f' <p class="move-subtitle" style="font-style:italic;">{subtitle}</p>'
441
+ f' </div>'
442
+ f'</div>',
443
+ unsafe_allow_html=True,
444
+ )
445
+
446
+ selected_move_label = st.selectbox(
447
+ "Select Move",
448
+ MOVE_LABELS,
449
+ format_func=lambda m: f"{MOVE_ICONS[m]} {m}",
450
+ key="move_selector",
451
+ )
452
+
453
+ move_questions = MOVES[selected_move_label]
454
+ selected_question = st.selectbox(
455
+ "Select question",
456
+ move_questions,
457
+ key="question_selector",
458
+ )
459
+
460
+ current_answer = st.text_area(
461
+ "Your answer",
462
+ height=110,
463
+ key="current_answer",
464
+ placeholder="Type your answer here…",
465
+ label_visibility="visible",
466
+ )
467
+
468
+ add_col, clear_col = st.columns([1, 1])
469
+ with add_col:
470
+ if st.button("βž• Add answer", use_container_width=True):
471
+ if not current_answer.strip():
472
+ st.warning("Please type an answer before adding.")
473
+ else:
474
+ st.session_state["qa_pairs"].append({
475
+ "move": selected_move_label,
476
+ "question": selected_question,
477
+ "answer": current_answer.strip(),
478
+ })
479
+ st.rerun()
480
+ with clear_col:
481
+ if st.button("πŸ—‘ Clear all", use_container_width=True):
482
+ st.session_state["qa_pairs"] = []
483
+ st.rerun()
484
+
485
+ st.markdown("---")
486
+
487
+ # ── Collected answers ────────────────────────────────────────────────────
488
+ qa_pairs: list[dict] = st.session_state["qa_pairs"]
489
+
490
+ if qa_pairs:
491
+ # Group by move for display
492
+ from collections import defaultdict
493
+ grouped: dict[str, list[tuple[int, dict]]] = defaultdict(list)
494
+ for idx, pair in enumerate(qa_pairs):
495
+ grouped[pair["move"]].append((idx, pair))
496
+
497
+ for move_label, cls in zip(MOVE_LABELS, MOVE_CLASSES):
498
+ pairs_in_move = grouped.get(move_label, [])
499
+ if not pairs_in_move:
500
+ continue
501
+ icon = MOVE_ICONS[move_label]
502
+ title_part = move_label.split("–")[0].strip()
503
+ st.markdown(
504
+ f'<div class="move-header {cls}" style="margin-bottom:0.3rem;">'
505
+ f' <div class="move-icon" style="font-size:1.2rem;">{icon}</div>'
506
+ f' <p class="move-title" style="margin:0;">{title_part} β€” {len(pairs_in_move)} answer(s)</p>'
507
+ f'</div>',
508
+ unsafe_allow_html=True,
509
+ )
510
+ for idx, pair in pairs_in_move:
511
+ col_text, col_btn = st.columns([11, 1])
512
+ with col_text:
513
+ st.markdown(
514
+ f'<div class="qa-item">'
515
+ f' <div class="qa-q">{pair["question"]}</div>'
516
+ f' <div class="qa-a">{pair["answer"]}</div>'
517
+ f'</div>',
518
+ unsafe_allow_html=True,
519
+ )
520
+ with col_btn:
521
+ if st.button("βœ•", key=f"rm_{idx}", help="Remove"):
522
+ st.session_state["qa_pairs"].pop(idx)
523
+ st.rerun()
524
+ else:
525
+ st.info("No answers yet β€” pick a question above, type your answer, and click **βž• Add answer**.")
526
+
527
+ st.markdown("---")
528
+ generate_clicked = st.button("✨ Generate introduction", use_container_width=True, type="primary")
529
+
530
+
531
+ # ── Right panel ─────────────────────────────────────────────────────────────
532
+ with right_col:
533
+ st.markdown('<div class="section-label">How it works</div>', unsafe_allow_html=True)
534
+ st.markdown(
535
+ "<div class='subtle-card'>"
536
+ "<b>1.</b> Choose a Move header, then pick a question from the dropdown.<br><br>"
537
+ "<b>2.</b> Type your answer and click <em>βž• Add answer</em>.<br><br>"
538
+ "<b>3.</b> Repeat across all three Moves for the best introduction.<br><br>"
539
+ "<b>4.</b> Click <em>✨ Generate introduction</em>.<br><br>"
540
+ "The app merges the instruction prompt + all your answers into one structured prompt "
541
+ "and calls the selected model."
542
+ "</div>",
543
+ unsafe_allow_html=True,
544
+ )
545
+
546
+ st.markdown("---")
547
+ st.markdown('<div class="section-label">Active Model</div>', unsafe_allow_html=True)
548
+ if backend == "ChatGPT (OpenAI)":
549
+ st.info(f"🟒 {selected_openai_model}")
550
+ elif backend == "Claude (Anthropic)":
551
+ st.info(f"🟠 {selected_claude_model}")
552
+ elif backend == "Gemini (Google)":
553
+ st.info(f"πŸ”΅ {selected_gemini_model}")
554
+ elif backend == "NVIDIA":
555
+ st.info(f"🟣 {selected_nvidia_model}")
556
+
557
+ # Move coverage
558
+ if qa_pairs:
559
+ st.markdown("---")
560
+ st.markdown('<div class="section-label">Move Coverage</div>', unsafe_allow_html=True)
561
+ for move_label, cls in zip(MOVE_LABELS, MOVE_CLASSES):
562
+ count = sum(1 for p in qa_pairs if p["move"] == move_label)
563
+ icon = "βœ…" if count else "β—‹"
564
+ short = move_label.split("–")[0].strip()
565
+ st.markdown(f"{icon} **{short}** β€” {count} answer(s)")
566
+
567
+
568
+ # ---------------------------------------------------------------------------
569
+ # Generation
570
+ # ---------------------------------------------------------------------------
571
+
572
+ if generate_clicked:
573
+ missing_key = False
574
+ if backend == "ChatGPT (OpenAI)" and not openai_api_key.strip():
575
+ st.error("Please enter your OpenAI API key in the sidebar.")
576
+ missing_key = True
577
+ elif backend == "Claude (Anthropic)" and not claude_api_key.strip():
578
+ st.error("Please enter your Anthropic API key in the sidebar.")
579
+ missing_key = True
580
+ elif backend == "Gemini (Google)" and not gemini_api_key.strip():
581
+ st.error("Please enter your Google AI API key in the sidebar.")
582
+ missing_key = True
583
+ elif backend == "NVIDIA" and not nvidia_api_key.strip():
584
+ st.error("Please enter your NVIDIA API key in the sidebar.")
585
+ missing_key = True
586
+
587
+ if not instruction_prompt.strip():
588
+ st.error("Please provide an instruction prompt.")
589
+ elif not qa_pairs:
590
+ st.error("Please add at least one answered question.")
591
+ elif not missing_key:
592
+ prompt = build_research_prompt(instruction_prompt, qa_pairs)
593
+ with st.expander("Show assembled prompt", expanded=False):
594
+ st.code(prompt, language="text")
595
+
596
+ try:
597
+ with st.status("Generating introduction…", expanded=True) as status:
598
+ if backend == "ChatGPT (OpenAI)":
599
+ status.write(f"Calling {selected_openai_model} via OpenAI API…")
600
+ introduction = generate_openai(
601
+ openai_api_key.strip(), OPENAI_MODELS[selected_openai_model], prompt, temperature
602
+ )
603
+ elif backend == "Claude (Anthropic)":
604
+ status.write(f"Calling {selected_claude_model} via Anthropic API…")
605
+ introduction = generate_claude(
606
+ claude_api_key.strip(), CLAUDE_MODELS[selected_claude_model], prompt, temperature
607
+ )
608
+ elif backend == "Gemini (Google)":
609
+ status.write(f"Calling {selected_gemini_model} via Google AI API…")
610
+ introduction = generate_gemini(
611
+ gemini_api_key.strip(), GEMINI_MODELS[selected_gemini_model], prompt, temperature
612
+ )
613
+ elif backend == "NVIDIA":
614
+ status.write(f"Calling {selected_nvidia_model} via NVIDIA API (streaming)…")
615
+ introduction = generate_nvidia(
616
+ nvidia_api_key.strip(), NVIDIA_MODELS[selected_nvidia_model],
617
+ prompt, temperature, nvidia_thinking
618
+ )
619
+ status.update(label="Generation complete", state="complete")
620
+ except Exception as error:
621
+ st.exception(error)
622
+ else:
623
+ st.markdown('<div class="section-label">Generated Introduction</div>', unsafe_allow_html=True)
624
+ st.text_area("Output", value=introduction, height=450, label_visibility="collapsed")
625
+ st.session_state["last_output"] = introduction
626
+ st.session_state["last_prompt"] = prompt
627
+ st.success("Done!")
628
+ else:
629
+ if "last_output" in st.session_state:
630
+ st.markdown('<div class="section-label">Most Recent Output</div>', unsafe_allow_html=True)
631
+ st.text_area("Output", value=st.session_state["last_output"], height=450, label_visibility="collapsed")
632
+ with st.expander("Show last assembled prompt", expanded=False):
633
+ st.code(st.session_state.get("last_prompt", ""), language="text")
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
1
+ streamlit>=1.36
2
+ openai>=1.0
3
+ anthropic>=0.100
4
+ google-genai>=1.0
5
+ requests>=2.31