File size: 8,612 Bytes
98306c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7417262
 
 
 
98306c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7417262
 
 
98306c5
 
 
7417262
 
98306c5
 
 
 
 
 
7417262
98306c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
"""
UI components for Streamlit application
"""

import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from typing import Dict, Optional
from config import (
    AVAILABLE_MODELS,
    DEFAULT_MODEL,
    DEFAULT_TEMPERATURE,
    DEFAULT_MAX_TOKENS,
    DEFAULT_SAMPLE_COUNT,
    SAMPLE_COUNT_OPTIONS,
    SIDEBAR_TITLE
)


def render_sidebar() -> Dict:
    """
    Render the sidebar with configuration options

    Returns:
        Dictionary with configuration values
    """
    st.sidebar.title(SIDEBAR_TITLE)

    # API Key section
    st.sidebar.subheader("πŸ”‘ OpenAI API Key")

    # Initialize session state for API key if not exists
    if "api_key" not in st.session_state:
        st.session_state.api_key = ""

    # Initialize available models in session state
    if "available_models" not in st.session_state:
        st.session_state.available_models = AVAILABLE_MODELS

    api_key_input = st.sidebar.text_input(
        "API Key",
        value=st.session_state.api_key,
        type="password",
        help="Enter your OpenAI API key",
        label_visibility="collapsed"
    )

    # Save/Validate button
    col1, col2 = st.sidebar.columns([1, 1])
    with col1:
        if st.button("πŸ’Ύ Save & Validate API Key", use_container_width=True):
            st.session_state.api_key = api_key_input
            st.session_state.api_key_validated = False  # Reset validation
            st.rerun()

    st.sidebar.divider()

    # Model and Parameters section
    st.sidebar.subheader("πŸ€– Model & Parameters")

    # Use models from session state
    available_models = st.session_state.available_models

    # Model selection
    model = st.sidebar.selectbox(
        "Model",
        options=available_models,
        index=available_models.index(DEFAULT_MODEL) if DEFAULT_MODEL in available_models else 0,
        help="Select the OpenAI model to use"
    )

    # Refresh models button
    if st.sidebar.button("πŸ”„ Refresh Models", help="Fetch available models from your API key"):
        st.session_state.refresh_models = True
        st.rerun()

    # Temperature
    temperature = st.sidebar.slider(
        "Temperature",
        min_value=0.0,
        max_value=2.0,
        value=DEFAULT_TEMPERATURE,
        step=0.1,
        help="Higher values make output more random, lower values more deterministic"
    )

    # Max Tokens
    max_tokens = st.sidebar.number_input(
        "Max Tokens",
        min_value=100,
        max_value=4000,
        value=DEFAULT_MAX_TOKENS,
        step=100,
        help="Maximum number of tokens in the response"
    )

    # Sample count
    sample_count = st.sidebar.selectbox(
        "Number of Samples (responses)",
        options=SAMPLE_COUNT_OPTIONS,
        index=SAMPLE_COUNT_OPTIONS.index(DEFAULT_SAMPLE_COUNT) if DEFAULT_SAMPLE_COUNT in SAMPLE_COUNT_OPTIONS else 2,
        help="Number of LLM responses to generate for analysis"
    )

    return {
        "api_key": st.session_state.api_key,
        "model": model,
        "temperature": temperature,
        "max_tokens": max_tokens,
        "sample_count": sample_count,
    }


def render_summary_stats(stats: Dict):
    """Render summary statistics in metric cards"""
    col1, col2, col3, col4 = st.columns(4)

    with col1:
        st.metric("Total Responses", stats["total_responses"])

    with col2:
        st.metric("Total Mentions", stats["total_mentions"])

    with col3:
        st.metric("Unique Brands", stats["unique_brands"])

    with col4:
        st.metric("Avg Brands/Response", f"{stats['avg_brands_per_response']:.1f}")


def render_brands_ranking_table(df: pd.DataFrame):
    """Render the brands ranking table"""
    st.subheader("πŸ“Š Brand Ranking")

    # Display dataframe with full width
    st.dataframe(
        df,
        use_container_width=True,
        hide_index=True,
        column_config={
            "Brand": st.column_config.TextColumn("Brand", width="medium"),
            "Visibility Rate": st.column_config.TextColumn("Visibility %", width="small"),
            "Top-1 Share": st.column_config.TextColumn("Top-1 %", width="small"),
            "Avg Position": st.column_config.TextColumn("Avg Pos", width="small"),
            "Mention Share": st.column_config.TextColumn("Mention %", width="small"),
            "Total Mentions": st.column_config.NumberColumn("Mentions", width="small"),
            "Appearances": st.column_config.NumberColumn("Appears", width="small"),
        }
    )


def render_visibility_chart(df: pd.DataFrame, top_n: int = 10):
    """Render visibility rate bar chart"""
    st.subheader(f"πŸ“ˆ Top {top_n} Brands by Visibility")

    # Take top N brands
    df_top = df.head(top_n).copy()

    # Convert percentage strings to floats for plotting
    df_top["Visibility_Value"] = df_top["Visibility Rate"].str.rstrip("%").astype(float)

    # Create bar chart
    fig = px.bar(
        df_top,
        x="Visibility_Value",
        y="Brand",
        orientation="h",
        labels={"Visibility_Value": "Visibility Rate (%)", "Brand": "Brand"},
        text="Visibility Rate",
        color="Visibility_Value",
        color_continuous_scale="Blues"
    )

    fig.update_layout(
        showlegend=False,
        height=400,
        yaxis={"categoryorder": "total ascending"}
    )

    fig.update_traces(textposition="outside")

    st.plotly_chart(fig, use_container_width=True)


def render_comparison_chart(df: pd.DataFrame, top_n: int = 10):
    """Render comparison chart with multiple metrics"""
    st.subheader(f"πŸ” Multi-Metric Comparison (Top {top_n})")

    # Take top N brands
    df_top = df.head(top_n).copy()

    # Convert percentage strings to floats
    df_top["Visibility_Value"] = df_top["Visibility Rate"].str.rstrip("%").astype(float)
    df_top["Top1_Value"] = df_top["Top-1 Share"].str.rstrip("%").astype(float)
    df_top["Mention_Value"] = df_top["Mention Share"].str.rstrip("%").astype(float)

    # Create grouped bar chart
    fig = go.Figure()

    fig.add_trace(go.Bar(
        name="Visibility Rate",
        x=df_top["Brand"],
        y=df_top["Visibility_Value"],
        text=df_top["Visibility Rate"],
        textposition="outside",
    ))

    fig.add_trace(go.Bar(
        name="Top-1 Share",
        x=df_top["Brand"],
        y=df_top["Top1_Value"],
        text=df_top["Top-1 Share"],
        textposition="outside",
    ))

    fig.add_trace(go.Bar(
        name="Mention Share",
        x=df_top["Brand"],
        y=df_top["Mention_Value"],
        text=df_top["Mention Share"],
        textposition="outside",
    ))

    fig.update_layout(
        barmode="group",
        xaxis_title="Brand",
        yaxis_title="Percentage (%)",
        height=500,
        legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
    )

    st.plotly_chart(fig, use_container_width=True)


def export_results_csv(df: pd.DataFrame, stats: Dict, filename: str = "brand_analysis") -> str:
    """
    Export results to CSV format

    Returns:
        CSV string
    """
    # Add summary stats as header comments
    csv_content = f"# LLM Brand Visibility Analysis\n"
    csv_content += f"# Total Responses: {stats['total_responses']}\n"
    csv_content += f"# Total Mentions: {stats['total_mentions']}\n"
    csv_content += f"# Unique Brands: {stats['unique_brands']}\n"
    csv_content += f"# Avg Brands per Response: {stats['avg_brands_per_response']:.2f}\n"
    csv_content += "#\n"

    # Add dataframe
    csv_content += df.to_csv(index=False)

    return csv_content


def export_results_txt(df: pd.DataFrame, stats: Dict) -> str:
    """
    Export results to TXT format

    Returns:
        TXT string
    """
    txt_content = "=" * 60 + "\n"
    txt_content += "LLM BRAND VISIBILITY ANALYSIS REPORT\n"
    txt_content += "=" * 60 + "\n\n"

    # Summary statistics
    txt_content += "SUMMARY STATISTICS:\n"
    txt_content += "-" * 60 + "\n"
    txt_content += f"Total Responses:          {stats['total_responses']}\n"
    txt_content += f"Total Mentions:           {stats['total_mentions']}\n"
    txt_content += f"Unique Brands:            {stats['unique_brands']}\n"
    txt_content += f"Avg Brands per Response:  {stats['avg_brands_per_response']:.2f}\n"
    txt_content += "\n"

    # Brand ranking
    txt_content += "BRAND RANKING:\n"
    txt_content += "-" * 60 + "\n"
    txt_content += df.to_string(index=False)
    txt_content += "\n\n"

    txt_content += "=" * 60 + "\n"
    txt_content += "End of Report\n"
    txt_content += "=" * 60 + "\n"

    return txt_content