Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/app"
schedule:
interval: "weekly"
11 changes: 11 additions & 0 deletions api/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ def home():
if not is_authenticated and not google.authorized and not github.authorized:
session.pop("user_info", None)

# If unauthenticated, show a simple landing page that invites sign-in or continuing as guest
if not is_authenticated:
return render_template("landing.j2", is_authenticated=False, user_info=None)

return render_template("chat.j2", is_authenticated=is_authenticated, user_info=user_info)


@auth_bp.route('/chat')
def chat():
"""Explicit chat route (renders main chat UI)."""
user_info, is_authenticated = validate_and_cache_user()
return render_template("chat.j2", is_authenticated=is_authenticated, user_info=user_info)


Expand Down
61 changes: 61 additions & 0 deletions app/public/css/landing.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.landing-container{
max-width:1200px;
margin:3rem auto;
display:grid;
grid-template-columns:1fr 500px;
gap:2.5rem;
align-items:center;
padding:0 1rem;
}

.hero-left{padding:1rem 0}
.hero-title{
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
font-weight:800;
font-size:4rem;
line-height:1.02;
margin:0 0 1rem 0;
color:var(--text-primary);
}
.hero-sub{
color:var(--text-secondary);
font-size:1.05rem;
max-width:44rem;
margin-bottom:1.5rem;
}
.hero-ctas{display:flex;gap:1rem;align-items:center}
.btn-pill{display:inline-block;padding:0.9rem 1.25rem;border-radius:999px;background:var(--falkor-primary);color:#fff;text-decoration:none;box-shadow:0 8px 20px rgba(91,107,192,0.14);}
.btn-ghost{background:transparent;color:var(--text-primary);text-decoration:none;padding:0.9rem 1.25rem}

.demo-card{background:var(--falkor-secondary);border-radius:12px;box-shadow:0 16px 30px rgba(11,18,32,0.06);padding:1rem;border:1px solid var(--border-color)}

/* Use a neutral themed surface for the inner area so it adapts to light/dark */
.demo-inner{background:var(--bg-tertiary);border-radius:8px;padding:1rem;border:1px solid var(--falkor-border-tertiary)}
.demo-label{font-size:0.9rem;color:var(--text-secondary);margin-bottom:0.5rem}

/* Use the theme's secondary surface for the white/black boxes so text contrast is correct in both themes */
.demo-question{background:var(--falkor-secondary);border-radius:6px;padding:0.75rem 1rem;border:1px solid var(--falkor-border-tertiary);color:var(--text-primary);height:120px;white-space:pre-wrap;font-family:monospace;font-size:0.95rem;overflow:auto;line-height:1.3}
.demo-sql{background:var(--falkor-secondary);border-radius:6px;padding:0.75rem 1rem;border:1px solid var(--falkor-border-tertiary);color:var(--text-primary);margin-top:0.8rem;font-size:0.9rem;overflow:auto;height:200px;line-height:1.25}
.demo-sql.typing{position:relative}
.demo-sql.typing::after{
content: '';
display:inline-block;
width:10px;
height:1.1em;
background:var(--falkor-primary);
margin-left:6px;
vertical-align:bottom;
animation: blink-caret 1s steps(1) infinite;
}

@keyframes blink-caret{
0%, 50% { opacity: 1 }
51%, 100% { opacity: 0 }
}
.demo-cta{margin-top:1rem;text-align:center}
.demo-cta .btn-full{display:inline-block;width:100%;padding:0.75rem;border-radius:8px;background:#e7f1ff;color:var(--falkor-primary);text-decoration:none}

@media (max-width:900px){
.landing-container{grid-template-columns:1fr;gap:1.25rem}
.hero-title{font-size:2.4rem}
}
119 changes: 119 additions & 0 deletions app/templates/landing.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
{% extends "base.j2" %}

{% block title %}QueryWeaver — Natural language to SQL{% endblock %}

{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/landing.css') }}">
{% endblock %}
Comment on lines +5 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure landing.css is actually served by Flask’s static setup (path likely mismatched).

Template links to static/css/landing.css, but the stylesheet lives at app/public/css/landing.css in this PR. Unless the Flask app’s static_folder points to app/public (or your build copies to app/static/css), this will 404 and the page will be unstyled.

Options:

  • Move file to app/static/css/landing.css (preferred), keeping this href unchanged.
  • Or configure Flask’s static_folder to app/public.

Quick verification:

If moving the file, run:


🏁 Script executed:

#!/bin/bash
# Does a static/ directory exist, and do we already serve CSS from there?
fd -t d '^static$' app -x echo {}
fd -t f 'landing.css' app -x echo {}

# Does the Flask app set static_folder to "public"?
rg -nP "Flask\s*\(\s*__name__.*static_folder\s*=\s*['\"]public['\"]" -C2

Length of output: 182


🏁 Script executed:

#!/bin/bash
git mkdir -p app/static/css 2>/dev/null || true
git mv app/public/css/landing.css app/static/css/landing.css

Length of output: 178


To pinpoint why the file move failed, let’s verify both the exact filesystem path and whether Git is tracking landing.css. Please run:


🏁 Script executed:

#!/bin/bash
# Show detailed listing of the CSS directory
echo "Contents of app/public/css/:"
ls -l app/public/css

# Confirm the exact path for landing.css
echo -e "\nSearching for landing.css under app/:"
fd -t f landing.css app -x echo {}

# Check if Git is tracking that file
echo -e "\nGit tracking status for app/public/css/landing.css:"
git ls-files app/public/css/landing.css || echo "→ Not tracked by Git"

Length of output: 1234


Fix static asset path for landing.css

Your template at app/templates/landing.j2 uses

<link rel="stylesheet" href="{{ url_for('static', filename='css/landing.css') }}">

but Flask’s default static_folder is app/static, and no override to public was found. As a result, the file under app/public/css/landing.css will 404.

We verified:

  • No app/static/ directory exists
  • app/public/css/landing.css is present and tracked by Git
  • No static_folder='public' setting in your Flask app ( confirmed this)

Required fix (preferred): Move the stylesheet into your static directory so it’s served correctly.

• Create the CSS folder and move the file:

mkdir -p app/static/css
git mv app/public/css/landing.css app/static/css/landing.css

• Update any other assets in app/public/css/ if they need to be served via url_for('static', …).

Alternative: Change your Flask app initialization (e.g. in app/__init__.py or wsgi.py) to:

app = Flask(__name__, static_folder='public')

Locations requiring attention:

  • app/public/css/landing.css
  • app/templates/landing.j2
  • Flask app entrypoint (where Flask(...) is called)

Move landing.css into Flask’s static folder

Your Jinja template at app/templates/landing.j2 uses:

<link rel="stylesheet" href="{{ url_for('static', filename='css/landing.css') }}">

but the stylesheet currently lives at app/public/css/landing.css, and there’s no evidence of static_folder='public' in your Flask setup. This will cause a 404.

Required actions:

  • Relocate the asset:
    mkdir -p app/static/css
    git mv app/public/css/landing.css app/static/css/landing.css
  • Verify any other CSS files under app/public/css/ are moved or served appropriately.
  • Confirm that url_for('static', filename='css/…') links to files in app/static/.

Alternatively, if you intend to keep assets under app/public, update your Flask app initialization:

app = Flask(__name__, static_folder='public')

Files needing changes:

  • app/public/css/landing.css
  • app/templates/landing.j2
  • Flask app entrypoint (where Flask(...) is instantiated)
🤖 Prompt for AI Agents
In app/templates/landing.j2 around lines 5-7 the template references
static/css/landing.css via url_for('static', filename='css/landing.css') but the
actual file currently lives at app/public/css/landing.css which will 404 unless
Flask is configured to serve public as the static folder; move
app/public/css/landing.css into app/static/css/landing.css (create
app/static/css if needed) and update repository (git mv), or alternatively
change the Flask app initialization to set static_folder='public' where
Flask(...) is instantiated; also scan app/public/css/ for other assets and
either move them to app/static/css or ensure they are served by the chosen
static_folder configuration so url_for('static', ...) resolves correctly.


{% block content %}
<div class="landing-container">

<!-- Left hero -->
<section class="hero-left">
<h1 class="hero-title">Transform Plain<br/>English into Powerful<br/>SQL.</h1>
<p class="hero-sub">The intelligent AI solution that connects your business questions to data across multiple databases. Ask a question in plain English and get runnable SQL and visual results.</p>

<div class="hero-ctas">
<a href="{{ url_for('auth.chat') }}" class="btn-pill">Sign Up for Free</a>
<a href="{{ url_for('auth.login_google') }}" class="btn-ghost">Sign In</a>
</div>
</section>

<!-- Right demo card -->
<aside class="demo-card">
<div class="demo-inner">
<div class="demo-label">Your Business Question:</div>
<div class="demo-question">Which customers from Germany who bought 'Chai' also have an open support ticket in Salesforce?</div>

<div class="demo-label">Generated SQL:</div>
<pre class="demo-sql">
SELECT c.companyName
FROM customers c
JOIN orders o ON c.customerID = o.customerID
JOIN order_details od ON o.orderID = od.orderID
JOIN products p ON od.productID = p.productID
JOIN salesforce_tickets st ON c.customerID = st.customerID
WHERE c.country = 'Germany'
AND p.productName = 'Chai'
AND st.status = 'Open';
</pre>

<div class="demo-cta">
<button id="demo-next" class="btn-full" type="button">Try another question</button>
</div>
</div>
<script>
(function(){
const examples = [
{
q: "Which customers from Germany who bought 'Chai' also have an open support ticket in Salesforce?",
sql: `SELECT c.companyName\nFROM customers c\nJOIN orders o ON c.customerID = o.customerID\nJOIN order_details od ON o.orderID = od.orderID\nJOIN products p ON od.productID = p.productID\nJOIN salesforce_tickets st ON c.customerID = st.customerID\nWHERE c.country = 'Germany'\n AND p.productName = 'Chai'\n AND st.status = 'Open';`
},
{
q: "Show me the top 5 customers by order count in London.",
sql: `SELECT c.companyName, COUNT(o.orderID) AS order_count\nFROM customers c\nJOIN orders o ON c.customerID = o.customerID\nWHERE c.city = 'London'\nGROUP BY c.companyName\nORDER BY order_count DESC\nLIMIT 5;`
},
{
q: "What is the quarterly revenue trend for the 'Beverages' category over the last two years?",
sql: `SELECT strftime('%Y-Q%q', o.orderDate) AS quarter,\n SUM(od.unitPrice * od.quantity) AS total_revenue\nFROM orders o\nJOIN order_details od ON o.orderID = od.orderID\nJOIN products p ON od.productID = p.productID\nJOIN categories cat ON p.categoryID = cat.categoryID\nWHERE cat.categoryName = 'Beverages'\n AND o.orderDate >= date('now', '-2 years')\nGROUP BY quarter\nORDER BY quarter;`
}
];

let idx = 0;
const qEl = document.querySelector('.demo-question');
const sEl = document.querySelector('.demo-sql');
const btn = document.getElementById('demo-next');

let typingTimer = null;
const typingSpeed = 15; // ms per character

function renderFull(i){
const ex = examples[i % examples.length];
if(qEl) qEl.textContent = ex.q;
if(sEl) {
sEl.classList.remove('typing');
sEl.textContent = ex.sql;
}
}

function typeSql(text){
if(!sEl) return;
// stop any previous typing
if(typingTimer) {
clearInterval(typingTimer);
typingTimer = null;
}
sEl.classList.add('typing');
sEl.textContent = '';
let pos = 0;
typingTimer = setInterval(() => {
pos += 1;
sEl.textContent = text.slice(0, pos);
if(pos >= text.length){
clearInterval(typingTimer);
typingTimer = null;
sEl.classList.remove('typing');
}
}, typingSpeed);
}

if(btn){
btn.addEventListener('click', function(){
idx = (idx + 1) % examples.length;
const ex = examples[idx];
if(qEl) qEl.textContent = ex.q;
// animate SQL typing
typeSql(ex.sql);
});
}

// initial render: show full first example without typing
renderFull(0);
})();
</script>
</div>
</aside>

</div>
{% endblock %}
24 changes: 19 additions & 5 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
{
"rewrites": [
{ "source": "/(.*)", "destination": "/api/index.py" }
{
"source": "/(.*)",
"destination": "/api/index.py"
}
],
"functions": {
"api/index.py": {
"maxDuration": 300
"builds": [
{
"src": "app/package.json",
"use": "@vercel/static-build",
"config": {
"distDir": "public"
}
},
{
"src": "api/index.py",
"use": "@vercel/python",
"config": {
"maxDuration": 300
}
Comment on lines +17 to +21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

maxDuration inside a builder’s config is ignored; it must be set via the functions property (builders and functions can’t be combined).

  • The "config.maxDuration" under the @vercel/python build does not configure function duration. For Python (and other non-inline runtimes), maxDuration must be set under the top-level "functions" key in vercel.json. (vercel.com)
  • However, Vercel explicitly disallows using "functions" together with "builds" in the same config. If you keep "builds", the functions property is not permitted, and your maxDuration setting won’t take effect. (vercel.com)

Recommend migrating away from "builds" and using:

  • top-level buildCommand + outputDirectory for the frontend,
  • top-level functions for Python duration.

Here’s a concrete replacement for the builds block that preserves behavior and enables maxDuration:

-  "builds": [
-    {
-      "src": "app/package.json",
-      "use": "@vercel/static-build",
-      "config": {
-        "distDir": "public"
-      }
-    },
-    {
-      "src": "api/index.py",
-      "use": "@vercel/python",
-      "config": {
-        "maxDuration": 300
-      }
-    }
-  ]
+  "buildCommand": "npm --prefix app run build",
+  "outputDirectory": "app/public",
+  "functions": {
+    "api/index.py": {
+      "maxDuration": 300
+    }
+  }

Notes:

  • buildCommand/outputDirectory are the modern way to declare how your static app is built and where the files end up. (vercel.com)
  • Removing "builds" avoids the “Conflicting functions and builds configuration” error and makes maxDuration effective. (vercel.com)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"src": "api/index.py",
"use": "@vercel/python",
"config": {
"maxDuration": 300
}
// remove the old "builds" array entirely:
- "builds": [
- {
- "src": "app/package.json",
- "use": "@vercel/static-build",
- "config": { "distDir": "public" }
- },
- {
- "src": "api/index.py",
- "use": "@vercel/python",
- "config": { "maxDuration": 300 }
- }
- ],
// add modern build + functions keys:
"buildCommand": "npm --prefix app run build",
"outputDirectory": "app/public",
"functions": {
"api/index.py": {
"maxDuration": 300
}
}
🤖 Prompt for AI Agents
In vercel.json around lines 17 to 21, the current "builds" block uses
"config.maxDuration" under the @vercel/python builder which is ignored and also
conflicts with the top-level "functions" property; replace the builds-based
approach by removing the "builds" entry, add top-level "buildCommand" and
"outputDirectory" to declare how the static frontend is built, and add a
top-level "functions" entry for your Python server with the desired
"maxDuration" (and any memory/other settings); this preserves build behavior
while making maxDuration effective and avoiding the builds/functions conflict.

}
}
]
}
Loading