2026-04-01 15:33:23 +02:00
<!doctype html>
< html lang = "en" >
< head >
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1" / >
< title > IfcOpenShell AI Assistant< / title >
< style >
* {
box-sizing : border-box ;
}
body {
font-family : system-ui , sans-serif ;
margin : 0 ;
}
button ,
input ,
select ,
textarea {
font : inherit ;
}
header {
padding : 12 px 16 px ;
border-bottom : 1 px solid #ddd ;
}
header input ,
header select {
padding : 8 px ;
}
main {
display : grid ;
grid-template-columns : 320 px 1 fr ;
height : 100 vh ;
}
. side {
border-right : 1 px solid #ddd ;
padding : 100 px 12 px 12 px 12 px ;
overflow : auto ;
background : #f9f9f9 ;
}
. chat {
display : flex ;
flex-direction : column ;
height : 100 % ;
}
. msgs {
flex : 1 ;
overflow : auto ;
padding : 16 px ;
}
. msg {
margin : 10 px 0 ;
}
. msg . role {
font-size : 12 px ;
opacity : 0.7 ;
margin : 12 px 0 4 px 0 ;
}
. role . user {
text-align : right ;
}
. msg . bubble {
padding : 0 ;
border-radius : 10 px ;
white-space : pre-wrap ;
}
. msg . user . bubble {
padding : 10 px 20 px ;
background : #eee ;
width : 50 % ;
margin-left : auto ;
border : solid 1 px #ddd ;
}
. msg . tool . bubble {
font-family : ui-monospace , SFMono-Regular , Menlo , monospace ;
font-size : 60 % ;
}
. composer {
display : flex ;
gap : 8 px ;
padding : 12 px ;
justify-content : center ;
}
. composer textarea {
flex : 1 ;
resize : none ;
height : 88 px ;
border : none ;
}
. composer button {
align-self : center ;
}
. composer . inner {
border : solid 1 px #ddd ;
border-radius : 20 px ;
padding : 10 px ;
display : flex ;
2026-04-02 13:27:54 +02:00
width : 100 % ;
}
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. spinner {
width : 18 px ;
height : 18 px ;
border : 2 px solid #aaa ;
border-top-color : #333 ;
border-radius : 50 % ;
animation : spin 0.7 s linear infinite ;
display : inline-block ;
2026-04-01 15:33:23 +02:00
}
. status {
font-size : 12 px ;
opacity : 0.7 ;
}
section > . row > label {
display : block ;
font-size : 12 px ;
opacity : 0.75 ;
margin-bottom : 6 px ;
font-weight : bold ;
}
section > . row > label . small {
font-weight : normal ;
display : block ;
font-size : 80 % ;
}
. side . row {
margin-bottom : 32 px ;
}
. row button {
padding : 8 px 10 px ;
}
# model {
background : white ;
color : gray ;
border : solid 1 px #eee ;
border-radius : 6 px ;
}
hr {
border : dashed 1 px #ddd ;
}
. btn-row {
display : flex ;
gap : 10 px ;
}
. btn-row > . btn ,
. btn-row > button . btn {
flex : 1 1 0 ;
min-width : 0 ;
}
. btn {
display : inline-flex ;
align-items : center ;
justify-content : center ;
gap : 8 px ;
font-size : 14 px ;
padding : 10 px 12 px ;
border-radius : 10 px ;
border : 1 px solid #d0d0d0 ;
background : #eee ;
cursor : pointer ;
user-select : none ;
text-decoration : none ;
}
. btn : hover {
background : #ddd ;
}
. btn : active {
transform : translateY ( 1 px ) ;
}
. btn-wide {
width : 100 % ;
}
. material-icons {
font-size : 18 px ;
line-height : 1 ;
}
/* Disabled state for label-button + normal buttons */
. btn . disabled ,
. btn : disabled {
opacity : 0.55 ;
cursor : not-allowed ;
pointer-events : none ;
}
. msg . tool . bubble {
max-height : 100 px ;
overflow : hidden ;
}
# input {
border : none ;
outline : none ;
}
# input : focus ,
# input : focus-visible {
outline : none ;
box-shadow : none ;
}
. code {
font-family : 'Courier New' , Courier , monospace ;
font-size : 90 % ;
background-color : #eee ;
border : solid 1 px #ddd ;
padding : 2 px ;
display : inline-block ;
}
< / style >
< link href = "https://fonts.googleapis.com/icon?family=Material+Icons" rel = "stylesheet" >
< / head >
< body >
< main >
< section class = "side" >
< div class = "row" >
2026-04-02 13:19:28 +02:00
< label > Provider< / label >
< select id = "provider" style = "width: 100%;" >
< option value = "openrouter" > OpenRouter< / option >
< option value = "openai" > OpenAI< / option >
< / select >
< / div >
< div class = "row" >
< label > OpenAI API key< span class = "small" > stored in browser memory< / span > < / label >
2026-04-01 15:33:23 +02:00
< input id = "apiKey" type = "password" placeholder = "sk-..." autocomplete = "off" style = "width: 100%;" / >
< / div >
< hr / >
< div class = "row" style = "margin-bottom: 10px;" >
< label > IFC model context< span class = "small" > stored in browser memory; only information extracted through MCP commands sent to OpenAI servers< / span > < / label >
< div class = "btn-row" >
< label class = "btn" id = "browseBtn" for = "ifcFile" role = "button" tabindex = "0" >
< span class = "material-icons" > folder_open< / span >
Browse
< / label >
< button class = "btn" id = "newModel" type = "button" >
< span class = "material-icons" > add_box< / span >
New IFC
< / button >
< / div >
< input id = "ifcFile" type = "file" accept = ".ifc,.ifczip,.ifcxml,.zip" hidden / >
< / div >
< div class = "row" >
< button class = "btn btn-wide" id = "downloadIfc" type = "button" >
< span class = "material-icons" > download< / span >
Download IFC
< / button >
< / div >
< hr / >
< div class = "row" >
< label > Tips< / label >
< div class = "status" >
• Upload an IFC, then ask “Summarize the model” or “List all IfcWalls”.< br / >
• Try “Add a new site and building named X” (will use ifc_edit).
< / div >
< / div >
< div class = "row" >
< label > About< / label >
< div class = "status" >
Chat with your IFC model. This app uses < span class = "code" > ifcopenshell-mcp< / span > to interact with an IFC model. IFC is in its raw-serialized form provides little textual context on the meaning of attributes, the intent behind element instances and the resulting geometrical form. By using the IfcOpenShell high-level API through an MCP-like interface (Model Context Protocol; allows for structured interaction with APIs and tools) these limitations are addressed and the model can be interrogated on a higher semantic abstraction level. You can also use < span class = "code" > ifcopenshell-mcp< / span > on your local machine with any LLM provider that supports the MCP protocol.
< / div >
< / div >
< / section >
< section class = "chat" >
< header >
< div class = "row" >
< img src = "https://ifcopenshell.org/assets/images/logo.png" style = "float: left; padding: 10px 20px 10px 0; " >
< strong > IfcOpenShell AI Assistant< / strong >
2026-04-02 13:19:28 +02:00
< select id = "model" > < / select >
2026-04-01 15:33:23 +02:00
< / div >
< div class = "row" >
Status: < span class = "status" id = "status" > Booting…< / span >
< / div >
< / header >
< div class = "msgs" id = "msgs" > < / div >
< div class = "composer" >
< div class = "inner" >
< textarea id = "input" placeholder = "Ask or instruct about the IFC model…" > < / textarea >
< button id = "send" class = "btn" > Send < span class = "material-icons" > send< / span > < / button >
< / div >
< / div >
< / section >
< / main >
< script type = "module" src = "./app.js" > < / script >
< / body >
< / html >